Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/pages/docs/routes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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).

<span id="build-with-routes" />
<span id="choose-a-funding-flow" />
Expand Down
143 changes: 143 additions & 0 deletions src/pages/docs/routes/examples/fx.mdx
Original file line number Diff line number Diff line change
@@ -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.

:::::
173 changes: 173 additions & 0 deletions src/pages/docs/routes/fx.mdx
Original file line number Diff line number Diff line change
@@ -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/<POOL_ADDRESS>/accounts/<ACCOUNT_ADDRESS>/access' \
--header "tempo-api-key: $TEMPO_API_KEY" \
--data-urlencode 'tokens=<SOURCE_TOKEN_ADDRESS>,<DESTINATION_TOKEN_ADDRESS>'
```

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.

<OpenApi.Playground operationId="quoteRoutesTransfer" anchors={false} />

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).

<OpenApi.Playground operationId="createRoutesTransfer" anchors={false} />

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

<OpenApi.Playground operationId="registerRoutesTransferSourceTransaction" anchors={false} />

The API checks the swap onchain and links it to the transfer.

### Confirm the swap

<OpenApi.Playground operationId="getRoutesTransfer" anchors={false} />

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": "<LIST_ADMIN_ADDRESS>",
"action": {
"type": "setMembers",
"tokens": ["<SOURCE_TOKEN_ADDRESS>", "<DESTINATION_TOKEN_ADDRESS>"],
"accounts": ["<ACCOUNT_ADDRESS>"],
"allowed": true
}
}
```

<OpenApi.Playground operationId="preparePropAmmAccess" anchors={false} />

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.

<OpenApi.Playground operationId="getPropAmmAccountAccess" anchors={false} />

## 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.
5 changes: 5 additions & 0 deletions vocs.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
},
]
Expand Down
Loading