탈중앙화 거래소에서 거래(Trade in the Decentralized Exchange)

이 튜토리얼에서는 탈중앙화 거래소(DEX)에서 토큰을 사고 파는 방법을 설명합니다.

요구 조건(Prerequisites)

  • XRP Ledger 네트워크에 연결해야 합니다. 이 튜토리얼에 표시된 대로 공용 서버를 사용하여 테스트할 수 있습니다.

  • 선호하는 클라이언트 라이브러리에 대한 시작하기 지침을 숙지하고 있어야 합니다. 이 페이지에서는 다음에 대한 예제를 제공합니다:

    • xrpl.js 라이브러리를 사용한 JavaScript. 설정 단계는 JavaScript를 사용하여 시작하기를 참조하세요.

    • xrpl-py 라이브러리를 사용하는 Python. 설정 단계는 Python을 사용하여 시작하기를 참조하세요.

    • 별도의 설정 없이 브라우저에서 대화형 단계를 따라 읽고 사용할 수도 있습니다.

예제 코드(Example Code)

이 튜토리얼의 모든 단계에 대한 전체 샘플 코드는 MIT 라이선스 에 따라 사용할 수 있습니다.

  • 코드 샘플을 참조하세요: 이 웹사이트의 소스 저장소에서 탈중앙화 거래소에서 트랜잭션하기를 참조하세요.

단계(Steps)

이 튜토리얼에서는 XRP를 판매하여 탈중앙화 거래소에서 대체 가능한 토큰을 구매하는 방법을 보여드리겠습니다. (다른 유형의 거래도 가능하지만, 예를 들어 토큰을 판매하려면 먼저 토큰을 보유해야 합니다.) 이 튜토리얼에서 사용하는 예시 토큰은 다음과 같습니다:

Currency Code
Issuer
Notes

TST

rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd

A test token pegged to XRP at a rate of approximately 10 XRP per 1 TST. The issuer has existing Offers on the XRP Ledger Testnet to buy and sell these tokens.

1. 네트워크에 연결(Connect to Network)

트랜잭션을 제출하려면 네트워크에 연결해야 합니다. 또한 일부 언어(JavaScript 포함)는 Ledger에서 찾을 수 있는 화폐 금액에 대한 계산을 수행하기 위해 고정밀 숫자 라이브러리가 필요합니다. 다음 코드는 적절한 종속성을 갖춘 지원되는 client library를 퍼블릭 XRP Ledger 테스트넷 서버에 연결하는 방법을 보여줍니다.

// In browsers, add the following <script> tags to the HTML to load dependencies
// instead of using require():
// <script src="https://unpkg.com/xrpl@2.2.0/build/xrpl-latest-min.js"></script>
// <script src='https://cdn.jsdelivr.net/npm/bignumber.js@9.0.2/bignumber.min.js'></script>
const xrpl = require('xrpl')
const BigNumber = require('bignumber.js')

// Wrap code in an async function so we can use await
async function main() {

  // Define the network client
  const client = new xrpl.Client("wss://s.altnet.rippletest.net:51233")
  await client.connect()

  // ... custom code goes here

  // Disconnect when done (If you omit this, Node.js won't end the process)
  client.disconnect()
}

main()

Note:

이 튜토리얼의 JavaScript 코드 샘플은 async/await 패턴을 사용합니다. await은 비동기 함수 내에서 사용해야 하므로 나머지 코드 샘플은 여기서 시작한 main() 함수 내에서 계속 사용하도록 작성되었습니다. 원하는 경우 async/await 대신 Promise 메소드 .then() 및 .catch()를 사용할 수도 있습니다.

이 튜토리얼에서는 다음 버튼을 클릭하여 연결합니다:

2. 자격증명 가져오기(Get Credentials)

XRP Ledger 에서 거래하려면 주소, 비밀 키, 약간의 XRP가 필요합니다. 개발 목적으로 다음 인터페이스를 사용하여 이러한 정보를 얻을 수 있습니다.

Caution:

리플은 테스트 목적으로만 Testnet and Devnet을 제공하며, 때때로 이러한 테스트 네트워크의 상태를 모든 잔액과 함께 초기화하기도 합니다. 예방책으로 테스트넷/데브넷과 메인넷에서 동일한 주소를 사용하지 마시기 바랍니다.

프로덕션용 소프트웨어를 구축할 때는 기존 계정을 사용하고 secure signing configuration을 사용하여 키를 관리해야 합니다. 다음 코드는 키를 사용하기 위해 월렛 인스턴스를 만드는 방법을 보여줍니다:

// Get credentials from the Testnet Faucet -----------------------------------
  console.log("Requesting address from the Testnet faucet...")
  const wallet = (await client.fundWallet()).wallet
  console.log(`Got address ${wallet.address}.`)
  // To use existing credentials, you can load them from a seed value, for
  // example using an environment variable as follows:
  // const wallet = xrpl.Wallet.fromSeed(process.env['MY_SEED'])

3. '제안' 조회(Look Up Offers)

토큰을 구매하거나 판매하기 전에 다른 사람들이 토큰을 어떤 가격에 구매하고 판매하는지 조회하여 다른 사람들이 토큰을 어떻게 평가하는지 파악하고 싶을 것입니다. XRP Ledger에서 book_offers method를 사용해 모든 화폐 쌍에 대한 기존 제안을 조회할 수 있습니다.

Tip:

엄밀히 말하면 이 단계는 오퍼를 올리기 위한 필수 조건은 아니지만, 실제 가치가 있는 물건을 거래하기 전에 현재 상황을 확인하는 것은 좋은 습관입니다.

다음 코드는 기존 Offers을 조회하고 제안된 Offers와 비교하여 어떻게 실행될지 예측하는 방법을 보여 줍니다:

// Define the proposed trade. ------------------------------------------------
  // Technically you don't need to specify the amounts (in the "value" field)
  // to look up order books using book_offers, but for this tutorial we reuse
  // these variables to construct the actual Offer later.
  const we_want = {
    currency: "TST",
    issuer: "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd",
    value: "25"
  }
  const we_spend = {
    currency: "XRP",
           // 25 TST * 10 XRP per TST * 15% financial exchange (FX) cost
    value: xrpl.xrpToDrops(25*10*1.15)
  }
  // "Quality" is defined as TakerPays / TakerGets. The lower the "quality"
  // number, the better the proposed exchange rate is for the taker.
  // The quality is rounded to a number of significant digits based on the
  // issuer's TickSize value (or the lesser of the two for token-token trades.)
  const proposed_quality = BigNumber(we_spend.value) / BigNumber(we_want.value)

  // Look up Offers. -----------------------------------------------------------
  // To buy TST, look up Offers where "TakerGets" is TST:
  const orderbook_resp = await client.request({
    "command": "book_offers",
    "taker": wallet.address,
    "ledger_index": "current",
    "taker_gets": we_want,
    "taker_pays": we_spend
  })
  console.log(JSON.stringify(orderbook_resp.result, null, 2))

  // Estimate whether a proposed Offer would execute immediately, and...
  // If so, how much of it? (Partial execution is possible)
  // If not, how much liquidity is above it? (How deep in the order book would
  //    other Offers have to go before ours would get taken?)
  // Note: These estimates can be thrown off by rounding if the token issuer
  // uses a TickSize setting other than the default (15). In that case, you
  // can increase the TakerGets amount of your final Offer to compensate.

  const offers = orderbook_resp.result.offers
  const want_amt = BigNumber(we_want.value)
  let running_total = BigNumber(0)
  if (!offers) {
    console.log(`No Offers in the matching book.
                 Offer probably won't execute immediately.`)
  } else {
    for (const o of offers) {
      if (o.quality <= proposed_quality) {
        console.log(`Matching Offer found, funded with ${o.owner_funds}
            ${we_want.currency}`)
        running_total = running_total.plus(BigNumber(o.owner_funds))
        if (running_total >= want_amt) {
          console.log("Full Offer will probably fill")
          break
        }
      } else {
        // Offers are in ascending quality order, so no others after this
        // will match, either
        console.log(`Remaining orders too expensive.`)
        break
      }
    }
    console.log(`Total matched:
          ${Math.min(running_total, want_amt)} ${we_want.currency}`)
    if (running_total > 0 && running_total < want_amt) {
      console.log(`Remaining ${want_amt - running_total} ${we_want.currency}
            would probably be placed on top of the order book.`)
    }
  }

  if (running_total == 0) {
    // If part of the Offer was expected to cross, then the rest would be placed
    // at the top of the order book. If none did, then there might be other
    // Offers going the same direction as ours already on the books with an
    // equal or better rate. This code counts how much liquidity is likely to be
    // above ours.

    // Unlike above, this time we check for Offers going the same direction as
    // ours, so TakerGets and TakerPays are reversed from the previous
    // book_offers request.
    const orderbook2_resp = await client.request({
      "command": "book_offers",
      "taker": wallet.address,
      "ledger_index": "current",
      "taker_gets": we_spend,
      "taker_pays": we_want
    })
    console.log(JSON.stringify(orderbook2_resp.result, null, 2))

    // Since TakerGets/TakerPays are reversed, the quality is the inverse.
    // You could also calculate this as 1/proposed_quality.
    const offered_quality = BigNumber(we_want.value) / BigNumber(we_spend.value)

    const offers2 = orderbook2_resp.result.offers
    let tally_currency = we_spend.currency
    if (tally_currency == "XRP") { tally_currency = "drops of XRP" }
    let running_total2 = 0
    if (!offers2) {
      console.log(`No similar Offers in the book. Ours would be the first.`)
    } else {
      for (const o of offers2) {
        if (o.quality <= offered_quality) {
          console.log(`Existing offer found, funded with
                ${o.owner_funds} ${tally_currency}`)
          running_total2 = running_total2.plus(BigNumber(o.owner_funds))
        } else {
          console.log(`Remaining orders are below where ours would be placed.`)
          break
        }
      }
      console.log(`Our Offer would be placed below at least
            ${running_total2} ${tally_currency}`)
      if (running_total > 0 && running_total < want_amt) {
        console.log(`Remaining ${want_amt - running_total} ${tally_currency}
              will probably be placed on top of the order book.`)
      }
    }
  }

Note:

XRP Ledger의 다른 사용자도 언제든지 거래를 할 수 있으므로, 이는 다른 변화가 없을 경우 발생할 수 있는 상황을 예상한 것입니다. 거래의 결과는 최종적으로 확정될 때까지 보장되지 않습니다.

다음 블록은 이러한 계산이 실제로 작동하는 모습을 보여줍니다:

4. '제안생성' 거래 전송 (Send OfferCreate Transaction)

실제로 거래를 하려면 OfferCreate transaction을 전송합니다. 이 경우 XRP를 사용하여 TST를 구매하고자 하므로 다음과 같이 매개변수를 설정해야 합니다:

필드
유형
설명

TakerPays

TakerPays 토큰 금액 객체 총 구매하고자 하는 화폐 금액입니다. 이 튜토리얼에서는 rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd가 발행한 일정 금액의 TST를 구매합니다.

TakerGets

총 지불할 화폐의 양을 지정합니다. 이 튜토리얼에서는 TST당 약 11.5 XRP 또는 그 이상을 지정해야 합니다.

다음 코드는 트랜잭션을 준비하고, 서명하고, 제출하는 방법을 보여줍니다:

// Send OfferCreate transaction ----------------------------------------------

  // For this tutorial, we already know that TST is pegged to
  // XRP at a rate of approximately 10:1 plus spread, so we use
  // hard-coded TakerGets and TakerPays amounts.

  const offer_1 = {
    "TransactionType": "OfferCreate",
    "Account": wallet.address,
    "TakerPays": we_want,
    "TakerGets": we_spend.value // since it's XRP
  }

  const prepared = await client.autofill(offer_1)
  console.log("Prepared transaction:", JSON.stringify(prepared, null, 2))
  const signed = wallet.sign(prepared)
  console.log("Sending OfferCreate transaction...")
  const result = await client.submitAndWait(signed.tx_blob)
  if (result.result.meta.TransactionResult == "tesSUCCESS") {
    console.log(`Transaction succeeded:
          https://testnet.xrpl.org/transactions/${signed.hash}`)
  } else {
    throw `Error sending transaction: ${result}`
  }

이 인터페이스를 사용하여 이전 단계에서 지정한 금액으로 트랜잭션을 전송할 수 있습니다:

5. 유효성 검사 대기(Wait for Validation)

대부분의 트랜잭션은 제출된 후 다음 Ledger 버전으로 승인되므로, 트랜잭션 결과가 최종 확정되기까지 4~7초가 소요될 수 있습니다. XRP Ledger이 사용 중이거나 네트워크 연결 상태가 좋지 않아 트랜잭션이 네트워크를 통해 릴레이되는 것이 지연되는 경우, 트랜잭션이 확인되는 데 더 오랜 시간이 걸릴 수 있습니다. (트랜잭션 만료를 설정하는 방법에 대한 자세한 내용은 Reliable Transaction Submission을 참조하세요.)

6. 메타데이터 확인(Check Metadata)

검증된 트랜잭션의 metadata를 사용해 트랜잭션이 정확히 어떤 일을 했는지 확인할 수 있습니다. (특히 탈중앙화 거래소를 사용할 때는 final result와 다를 수 있으므로 임시 트랜잭션 결과의 메타데이터를 사용하지 마세요). 오퍼 생성 트랜잭션의 경우 예상되는 결과는 다음과 같습니다:

  • 제안의 일부 또는 전부가 Ledger에 있는 기존 제안과 일치하여 채워졌을 수 있습니다.

  • 매칭되지 않은 나머지 제안이 있다면 Ledger에 배치되어 새로운 매칭 제안을 기다리고 있을 것입니다.

  • 매칭될 수 있는 만료되었거나 펀딩되지 않은 제안을 제거하는 등 다른 부기 작업이 발생했을 수도 있습니다.

다음 코드는 트랜잭션의 메타데이터를 확인하는 방법을 보여줍니다:

// Check metadata ------------------------------------------------------------
  // In JavaScript, you can use getBalanceChanges() to help summarize all the
  // balance changes caused by a transaction.
  const balance_changes = xrpl.getBalanceChanges(result.result.meta)
  console.log("Total balance changes:", JSON.stringify(balance_changes, null,2))

  // Helper to convert an XRPL amount to a string for display
  function amt_str(amt) {
    if (typeof amt == "string") {
      return `${xrpl.dropsToXrp(amt)} XRP`
    } else {
      return `${amt.value} ${amt.currency}.${amt.issuer}`
    }
  }

  let offers_affected = 0
  for (const affnode of result.result.meta.AffectedNodes) {
    if (affnode.hasOwnProperty("ModifiedNode")) {
      if (affnode.ModifiedNode.LedgerEntryType == "Offer") {
        // Usually a ModifiedNode of type Offer indicates a previous Offer that
        // was partially consumed by this one.
        offers_affected += 1
      }
    } else if (affnode.hasOwnProperty("DeletedNode")) {
      if (affnode.DeletedNode.LedgerEntryType == "Offer") {
        // The removed Offer may have been fully consumed, or it may have been
        // found to be expired or unfunded.
        offers_affected += 1
      }
    } else if (affnode.hasOwnProperty("CreatedNode")) {
      if (affnode.CreatedNode.LedgerEntryType == "RippleState") {
        console.log("Created a trust line.")
      } else if (affnode.CreatedNode.LedgerEntryType == "Offer") {
        const offer = affnode.CreatedNode.NewFields
        console.log(`Created an Offer owned by ${offer.Account} with
          TakerGets=${amt_str(offer.TakerGets)} and
          TakerPays=${amt_str(offer.TakerPays)}.`)
      }
    }
  }
  console.log(`Modified or removed ${offers_affected} matching Offer(s)`)

이 인터페이스를 사용하여 테스트해 볼 수 있습니다:

7. 잔액 및 제안 확인(Check Balances and Offers)

또한 가장 최근에 확인된 Ledger을 기준으로 계정의 잔액과 미결제 Offers를 조회할 수 있는 좋은 시기입니다. 여기에는 동일한 Ledger 버전에서 실행된 다른 거래뿐만 아니라 거래로 인해 발생한 모든 변경사항이 표시됩니다.

다음 코드는 account_lines method를 사용하여 잔액을 조회하고 account_offers method를 사용하여 제안 조회하는 방법을 보여줍니다.

// Check balances ------------------------------------------------------------
  console.log("Getting address balances as of validated ledger...")
  const balances = await client.request({
    command: "account_lines",
    account: wallet.address,
    ledger_index: "validated"
    // You could also use ledger_index: "current" to get pending data
  })
  console.log(JSON.stringify(balances.result, null, 2))

  // Check Offers --------------------------------------------------------------
  console.log(`Getting outstanding Offers from ${wallet.address} as of validated ledger...`)
  const acct_offers = await client.request({
    command: "account_offers",
    account: wallet.address,
    ledger_index: "validated"
  })
  console.log(JSON.stringify(acct_offers.result, null, 2))

이 인터페이스를 사용하여 테스트해 볼 수 있습니다:

Last updated