diff --git a/src/pages/docs/routes.mdx b/src/pages/docs/routes.mdx index e2934bfd2..0a0c67c29 100644 --- a/src/pages/docs/routes.mdx +++ b/src/pages/docs/routes.mdx @@ -31,7 +31,7 @@ Routes is in beta. [Contact Tempo](https://tempo.xyz/contact) to discuss access A route connects a source network and token to a destination network and token. Routes prepares the funding instructions or source-chain calls, then tracks delivery to the recipient. A successful source transaction starts the route; your app confirms completion at the destination. -For payments that stay on Tempo, use [Send payments](/docs/guide/payments/send-a-payment) or [Swap stablecoins](/docs/guide/stablecoin-dex/executing-swaps) instead. +For payments that stay on Tempo, use [Send payments](/docs/guide/payments/send-a-payment) or [Swap stablecoins](/docs/guide/stablecoin-dex/executing-swaps) instead. Routes also swaps stablecoins between currencies on Tempo through [FX routes](/docs/routes/fx). diff --git a/src/pages/docs/routes/examples/fx.mdx b/src/pages/docs/routes/examples/fx.mdx new file mode 100644 index 000000000..a3528171a --- /dev/null +++ b/src/pages/docs/routes/examples/fx.mdx @@ -0,0 +1,143 @@ +--- +title: "FX swap example" +seoTitle: "Routes FX swap example | Tempo Docs" +description: Run an FX swap on Tempo from a terminal with curl and jq, from the account check to the completed transfer. +--- + +# FX swap example + +Run every step of [Swap currencies](/docs/routes/fx) from a terminal. That guide explains each step; this page keeps the commands together. You sign the source calls with your own signer. + +## Set the FX example variables + +Set `TEMPO_API_KEY` to a key with `routes:read`, `routes:write`, and `data:read` from [Set up Routes](/docs/routes/api-setup). Then set: + +| Variable | Value | +| --- | --- | +| `SOURCE_TOKEN`, `DESTINATION_TOKEN` | Token addresses on Tempo from the [route directory](/docs/routes/networks). OUSD is `0x20c0000000000000000000006a37da5c996874be`. EURC.e is `0x20c0000000000000000000001621e21f71cf12fb`. | +| `FX_ACCOUNT` | The listed account that sends and receives the swap. | +| `FX_AMOUNT` | Source amount in base units. `1000000` is 1 OUSD. | +| `POOL_ADDRESS` | The route's pool. Tempo provides it. | + +## Swap OUSD for EURC.e + +:::::steps + +### Check the account + +```bash +curl --get \ + "https://api.tempo.xyz/v1/prop-amms/${POOL_ADDRESS}/accounts/${FX_ACCOUNT}/access" \ + --header "tempo-api-key: ${TEMPO_API_KEY}" \ + --data-urlencode "tokens=${SOURCE_TOKEN},${DESTINATION_TOKEN}" \ + --data-urlencode 'chainId=4217' +``` + +Continue when the response has `"allowed": true`. The request checks your organization's saved list. If your organization uses the pool's default list, add `--data-urlencode 'sponsorId=0'`. + +### Get a quote + +```bash +curl --get 'https://api.tempo.xyz/v1/routes/transfers/quote' \ + --header "tempo-api-key: ${TEMPO_API_KEY}" \ + --data-urlencode 'sourceChain=tempo' \ + --data-urlencode "sourceToken=${SOURCE_TOKEN}" \ + --data-urlencode 'destinationChain=tempo' \ + --data-urlencode "destinationToken=${DESTINATION_TOKEN}" \ + --data-urlencode "sender=${FX_ACCOUNT}" \ + --data-urlencode "recipient=${FX_ACCOUNT}" \ + --data-urlencode "amount=${FX_AMOUNT}" \ + --data-urlencode 'mode=exactSource' +``` + +The quote expires within seconds. Run the next two steps right after it. + +### Create the transfer + +Set `ROUTE_REQUEST_KEY` to the [idempotency key](/docs/routes/api-setup#set-idempotency-keys) for this swap. + +```bash +jq -n \ + --arg sourceToken "$SOURCE_TOKEN" \ + --arg destinationToken "$DESTINATION_TOKEN" \ + --arg sender "$FX_ACCOUNT" \ + --arg recipient "$FX_ACCOUNT" \ + --arg amount "$FX_AMOUNT" \ + '$ARGS.named + {sourceChain: "tempo", destinationChain: "tempo", mode: "exactSource"}' \ + > fx-request.json + +curl --fail-with-body 'https://api.tempo.xyz/v1/routes/transfers' \ + --header "tempo-api-key: ${TEMPO_API_KEY}" \ + --header "idempotency-key: ${ROUTE_REQUEST_KEY}" \ + --header 'Content-Type: application/json' \ + --data @fx-request.json \ + --output fx-transfer.json + +jq '{id, expiresAt: .quote.expiresAt, calls: .action.calls}' fx-transfer.json +``` + +The output lists the transfer `id`, its expiry, and the two calls to sign. + +### Sign and submit + +Submit both calls from `FX_ACCOUNT` before that expiry, as one Tempo Transaction. Keep the transaction hash. + +### Register the source transaction + +Set `ROUTE_TRANSFER_ID` to the returned `id` and `SOURCE_TRANSACTION` to the hash. + +```bash +jq -n --arg transactionHash "$SOURCE_TRANSACTION" '$ARGS.named' > fx-source.json + +curl --fail-with-body \ + "https://api.tempo.xyz/v1/routes/transfers/${ROUTE_TRANSFER_ID}/source-transactions" \ + --header "tempo-api-key: ${TEMPO_API_KEY}" \ + --header 'Content-Type: application/json' \ + --data @fx-source.json +``` + +### Confirm the swap + +```bash +curl "https://api.tempo.xyz/v1/routes/transfers/${ROUTE_TRANSFER_ID}" \ + --header "tempo-api-key: ${TEMPO_API_KEY}" +``` + +The swap is done when `status` is `completed`. + +::::: + +## Add accounts to the list + +The list's admin account adds `FX_ACCOUNT` before its first swap. Set `LIST_ADMIN` to the admin account's address. The key also needs `propamm:write`. + +:::::steps + +### Prepare the change + +```bash +jq -n \ + --arg sender "$LIST_ADMIN" \ + --arg account "$FX_ACCOUNT" \ + --arg sourceToken "$SOURCE_TOKEN" \ + --arg destinationToken "$DESTINATION_TOKEN" \ + '{sender: $sender, action: {type: "setMembers", tokens: [$sourceToken, $destinationToken], accounts: [$account], allowed: true}}' \ + > fx-access.json + +curl --fail-with-body \ + "https://api.tempo.xyz/v1/prop-amms/${POOL_ADDRESS}/access/prepare" \ + --header "tempo-api-key: ${TEMPO_API_KEY}" \ + --header 'Content-Type: application/json' \ + --data @fx-access.json \ + --output fx-access-change.json + +jq '{effect, transaction}' fx-access-change.json +``` + +The output states the change and the unsigned transaction. The change applies to your organization's saved list. + +### Sign and send the transaction + +Send `transaction.data` to `transaction.to` from `LIST_ADMIN`. The API doesn't sign or send it. Run the account check again after the transaction confirms. + +::::: diff --git a/src/pages/docs/routes/fx.mdx b/src/pages/docs/routes/fx.mdx new file mode 100644 index 000000000..8c06fe5b7 --- /dev/null +++ b/src/pages/docs/routes/fx.mdx @@ -0,0 +1,173 @@ +--- +title: Swap currencies +seoTitle: Swap Currencies | Tempo Docs +description: Swap a stablecoin in one currency for a stablecoin in another on Tempo, such as OUSD for EURC.e, with a Routes transfer. +--- + +import { OpenApi } from 'vocs' + +# Swap currencies + +Swap a stablecoin in one currency for a stablecoin in another on Tempo, such as OUSD for EURC.e. + +An FX route is a [Routes](/docs/routes) route with both ends on Tempo and a different currency on each end. You quote it, create a transfer, and sign the returned calls, as in [Make a transfer](/docs/routes/transfers). The source transaction is the swap, so the sender receives the destination stablecoin when it confirms. + +:::warning +FX routes fill swaps only from listed accounts. [Contact Tempo](https://tempo.xyz/contact) to enable a route and set up your organization's account list. +::: + +## How FX routes differ + +- **Listed senders.** Only accounts on your organization's [account list](#account-lists-on-fx-routes) can swap. A quote for an unlisted `sender` returns `404`. +- **Same sender and recipient.** The swap pays the account that sends it. Set `recipient` to the `sender` address. +- **Short quotes.** A quote expires within seconds. The swap call carries that expiry and reverts after it. +- **Price in the amounts.** The quoted source and destination amounts include the price. The route adds no separate fee. + +## Account lists on FX routes + +An account list names the accounts that can swap on one pair of a pool. The pool is the contract that fills an FX route's swaps. A pair is the two stablecoins it swaps. + +- **Default list.** Every pool has a default list with sponsor ID `0`. The pool's owner manages it. +- **Organization lists.** A pool can have further lists. Each has its own sponsor ID and its own admin account. + +A swap runs under exactly one list, and the sender must be on that list. Your organization uses either the pool's default list or its own list. Tempo saves that choice for each pool and pair when it enables your access. You can't change it through the API. + +Routes quotes and creates swaps for your organization's API keys under the saved list. Quote and transfer requests take no list parameter. An organization with no saved list uses the pool's default list. + +Once your organization has its own list on a pair, the default list no longer applies to your keys there. An account that's only on the default list gets no quote through your keys on that pair. + +## Prepare an FX swap + +Before you start, have: + +- A Tempo API key with `routes:read`, `routes:write`, and `data:read` from [Set up Routes](/docs/routes/api-setup). +- An FX route from [Supported routes](/docs/routes/networks), such as OUSD to EURC.e on Tempo, and its two token addresses. +- The route's pool address. Tempo provides it when it enables your access. +- A signer for a listed account that holds the source stablecoin and a fee token. + +Every step also runs from a terminal with curl and jq, as the [FX swap example](/docs/routes/examples/fx) shows. + +## Swap one stablecoin for another + +:::::steps + +### Check the account + +Name the pair by its two token addresses, in either order. + +```bash +curl --get \ + 'https://api.tempo.xyz/v1/prop-amms//accounts//access' \ + --header "tempo-api-key: $TEMPO_API_KEY" \ + --data-urlencode 'tokens=,' +``` + +The API checks the account against your organization's saved list. A listed account returns `"member": true`, `"allowed": true`, and `"reason": "allowed"`. Routes runs the same check when it quotes. Otherwise `reason` and `explanation` name what's missing, such as `notListed`. + +Without a saved list, the request returns `400` with code `propamm_invalid`. If your organization uses the pool's default list, add `sponsorId=0`. + +### Get a quote + +Send the account as both `sender` and `recipient`. Amounts are in base units. Routes quotes under your organization's list, so the request names no pool or list. + + + +Show the source amount, the destination amount, and `destinationAmountMin`. Create the transfer before `quote.expiresAt`. + +### Create the transfer + +Send the same inputs with an [idempotency key](/docs/routes/api-setup#set-idempotency-keys). + + + +Save the transfer `id` and its `action`. For an exact-source swap, `action.calls` holds two calls. The first approves the source stablecoin. The second swaps it. + +### Sign and submit + +Submit both calls from the sender account before the transfer's `quote.expiresAt`. Send them as one [Tempo Transaction](/docs/guide/tempo-transaction) so the approval and the swap land together. [Tempo Wallet](/docs/accounts/wallets#connect-tempo-wallet) does this for you. + +If the swap call reverts after the expiry, create a new transfer from a new quote. + +### Register the source transaction + + + +The API checks the swap onchain and links it to the transfer. + +### Confirm the swap + + + +Keep the payment pending until `status` is `completed`. Reconcile `destinationAmount` against the account's balance change. + +::::: + +## Manage the account list + +A list has one admin account. The admin adds or removes up to 50 accounts in one request. The key needs `propamm:write`, which an organization owner issues. + +:::::steps + +### Prepare the change + +Send the list's admin account as `sender`. Name the pair by its two token addresses, in either order. + +```json +{ + "sender": "", + "action": { + "type": "setMembers", + "tokens": ["", ""], + "accounts": [""], + "allowed": true + } +} +``` + + + +The API applies the change to your organization's saved list. It simulates the change and returns an unsigned `transaction`. `effect` names the resolved pair index and sponsor ID, such as `List 1 account(s) on pair 0 under sponsor 5.` + +To remove accounts, send the same request with `"allowed": false`. + +### Sign and send the transaction + +The admin account sends a transaction to `transaction.to` with `transaction.data`. The API doesn't sign or send it. + +### Confirm the listing + +Repeat the [account check](#check-the-account) after the transaction confirms. A listed account returns `"allowed": true`. + +::::: + +## Name a pair or list + +Requests that omit `sponsorId` use your organization's saved list. Requests with `tokens` resolve the pair from its two token addresses. Name either explicitly when the API can't resolve it, such as a pool with two pairs for the same tokens. + +- **Sponsor ID.** Pass `sponsorId` as a decimal string to name a list, such as `"0"` for the pool's default list. The account check takes it as a query parameter. The access change takes it in `action`. +- **Pair index.** Pass `pairIndex` in place of `tokens` in the access change. Send exactly one of the two. The account check takes the pair index in its path. + + + +## Account list errors + +An access change the API can't resolve or simulate returns one of these errors. The account check returns the same `400` when your organization has no saved list. + +| Status | Code | Cause | Fix | +| --- | --- | --- | --- | +| `400` | `propamm_invalid` | The request omits `sponsorId`, and your organization has no saved list on that pool and pair. | Pass `sponsorId`, or ask Tempo to set the list. | +| `404` | `propamm_not_found` | The pool has no active pair for the two tokens. | Check both token addresses against the route. | +| `409` | `propamm_conflict` | More than one pair swaps the two tokens. | Pass `pairIndex` in place of `tokens`. | +| `409` | `propamm_rejected` | `sender` isn't the list's admin account. | Send the list's admin account as `sender`. | + +## FX quote errors + +A refused quote returns `404` with code `quote_not_available`. Its `details` name the field to change. + +| `details` path | Cause | Fix | +| --- | --- | --- | +| `sender` | The account isn't on your organization's list. | Check the account, then ask the list admin to add it. | +| `recipient` | The recipient differs from the sender. | Set `recipient` to the `sender` address. | +| `amount` | The pool can't fill that size now. | Lower the amount or retry later. | + +An FX transfer reports the same [transfer statuses](/docs/routes/delivery) as other Routes transfers, such as `action-required` when it needs attention. diff --git a/vocs.config.ts b/vocs.config.ts index cf47bef52..8144ec71f 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -852,11 +852,16 @@ export default defineConfig({ { text: 'Track delivery', link: '/docs/routes/delivery' }, ], }, + { + text: 'FX', + items: [{ text: 'Swap currencies', link: '/docs/routes/fx' }], + }, { text: 'cURL examples', items: [ { text: 'Deposit address example', link: '/docs/routes/examples/deposits' }, { text: 'Transfer example', link: '/docs/routes/examples/transfers' }, + { text: 'FX swap example', link: '/docs/routes/examples/fx' }, ], }, ]