Skip to content

Tracking a Transfer

Prerequisites

Before tracking a transfer, you need to send it. See Sending a Transfer to get the Transfer ID.

Transfer ID

Technical Details

Transfer ID is the transaction hash of the event on the Proxy contract.

┌─────────────────────────────────────────────────────────────────┐
│                      Proxy Contract                             │
│                                                                 │
│  1. Receives tokens via transferNotification()                  │
│  2. Calls _emitTvmEvent() or _deployEvmEvent()                  │
│  3. Emits TvmTvmNative or TvmTvmAlien event                     │
│  4. Transaction hash of this event = TRANSFER ID               │
└─────────────────────────────────────────────────────────────────┘

Location in Blockchain

Transaction (on Proxy contract) ← Transaction Hash = TRANSFER ID
├── In Message (incoming tokens)
├── Out Messages
│   ├── Internal Messages (internal transfers)
│   └── External Out Message ← EVENT (TvmTvmNative/TvmTvmAlien)
└── Account (updated state)

Event Types

EventTokenTypeDescription
TvmTvmNativeNativeNative token transfer (lock → mint)
TvmTvmAlienAlienAlien token transfer (burn → unlock)

Transfer Statuses

Two Status Levels

The system has two status levels:

LevelWhere StoredPurpose
TransferStatusBridge Aggregator APIAggregated status for UI/integrations
Event Contract StatusOn-chain (Event contract)Detailed event verification state

Relationship: Bridge Aggregator API aggregates on-chain Event contract statuses into a simplified TransferStatus:

Event Contract Status                    →    TransferStatus (API)
─────────────────────────────────────────────────────────────────────
Initializing, Pending, Verified          →    Pending
Confirmed, LiquidityProvided             →    Completed
Rejected, Cancelled                      →    Failed

TransferStatus (API level)

Used in Bridge Aggregator API responses (/v2/transfers/status, /v2/transfers/search).

StatusCodeDescription
Pending1Event created, awaiting confirmation in destination network
Completed2Transfer fully completed (tokens delivered)
Failed3Transfer rejected by relays

Event Contract Status (on-chain level)

Detailed Event contract statuses in the blockchain. Used to understand the verification stage of the event.

StatusCodeDescription→ TransferStatus
Initializing0Event contract deployedPending
Pending1Awaiting verificationPending
Confirmed2Confirmed, proxy called, tokens deliveredCompleted
Rejected3RejectedFailed
Cancelled4Cancelled by userFailed
LimitReached5Daily limit exceeded, requires liquidityPending
LiquidityRequested6Liquidity request createdPending
LiquidityProvided7Liquidity provided, tokens deliveredCompleted
Verified8Merkle proof verified, awaiting token deployPending

Trustless transition: PendingVerifiedConfirmed

Status Transition Diagram

Status Transition Diagram

Getting Status by Transfer ID

API Endpoint

POST https://tetra-history-api.chainconnect.com/v2/transfers/status
Content-Type: application/json

Test Environment

For testing, use https://history-api-test.chainconnect.com/v2/transfers/status

Request Parameters

Important

According to the OpenAPI specification, the field is called outgoingTransactionHash (not outgoingMessageHash)

ParameterTypeRequiredDescription
outgoingTransactionHashstringTransfer ID (hex, 64 characters)
dappChainIdnumberChain ID of source network (TON = -239, Tycho = 2000)
timestampCreatedFromnumberUnix timestamp for filtering (optional)

Request Example (TvmTvm)

bash
curl -X POST 'https://tetra-history-api.chainconnect.com/v2/transfers/status' \
  -H 'Content-Type: application/json' \
  -d '{
    "tvmTvm": {
      "outgoingTransactionHash": "abc123def456...",
      "dappChainId": -239,
      "timestampCreatedFrom": null
    }
  }'

Response Example

json
{
  "transfer": {
    "tvmTvm": {
      "transferStatus": "Pending",
      "timestampCreatedAt": 1767972717,
      "outgoing": {
        "tokenType": "Native",
        "chainId": -239,
        "userAddress": "0:3333...cccc",
        "tokenAddress": "0:1111...aaaa",
        "proxyAddress": "0:6666...ffff",
        "volumeExec": "0.1000",
        "volumeUsdtExec": "0",
        "feeVolumeExec": "0",
        "messageHash": "abc123def456...",
        "transactionHash": "def789abc012..."
      },
      "incoming": {
        "tokenType": "Alien",
        "chainId": 2000,
        "userAddress": "0:2222...bbbb",
        "tokenAddress": "",
        "proxyAddress": "",
        "volumeExec": "0",
        "volumeUsdtExec": "0",
        "feeVolumeExec": null,
        "messageHash": "",
        "transactionHash": null
      }
    }
  },
  "updatedAt": 1767972720,
  "notInstantTransfer": null,
  "proofPayload": {
    "txBlockProof": "te6ccgECCg...",
    "txProof": "te6ccgEBBw...",
    "messageHash": "abc123def456...",
    "outMessageIndex": 0,
    "event": {
      "tokenType": "Native",
      "chainId": 2000,
      "token": "0:1111...aaaa",
      "amount": "99000",
      "recipient": "0:2222...bbbb",
      "value": "150000000",
      "expectedGas": "0",
      "remainingGasTo": "0:3333...cccc",
      "sender": "0:3333...cccc",
      "payload": "te6ccgEBAQEAAgAAAA==",
      "nativeProxyWallet": "0:9999...2222",
      "name": "Tether USD",
      "symbol": "USD₮",
      "decimals": 6
    },
    "abiTx": {
      "tx": "te6ccgEBBwEA6AABiw/X8c8...",
      "executionAddress": "0:8888...1111",
      "attachedValue": "2000000000",
      "abiMethod": "deployEvent",
      "abi": "...",
      "params": "{\"_eventVoteData\":{...}}"
    }
  }
}

Response Field Descriptions

transfer.tvmTvm — transfer data

FieldTypeDescription
transferStatusstringStatus: "Pending", "Completed", "Failed"
timestampCreatedAtnumberUnix timestamp of transfer creation

outgoing — source side data

All fields are non-nullable. Default values ("" for strings, "0" for numbers) are used when data is unavailable.

FieldTypeDescription
tokenTypestring"Native" or "Alien"
chainIdnumberNetwork Chain ID
userAddressstringSender address
tokenAddressstringToken address in source network
proxyAddressstringProxy contract address
volumeExecstringAmount in human-readable format
volumeUsdtExecstringAmount in USDT equivalent
feeVolumeExecstringFee charged
messageHashstringEvent message hash
transactionHashstringTransaction hash (= Transfer ID)

incoming — destination side data

Most fields are non-nullable (defaults "" / "0" while transfer is not yet completed). Only feeVolumeExec and transactionHash are nullable.

FieldTypeDescription
tokenTypestring"Native" or "Alien"
chainIdnumberNetwork Chain ID
userAddressstringRecipient address
tokenAddressstringToken address in destination network (empty string until completed)
proxyAddressstringProxy contract address (empty string until completed)
volumeExecstringAmount in human-readable format ("0" until completed)
volumeUsdtExecstringAmount in USDT equivalent ("0" until completed)
feeVolumeExecstring | nullFee charged
messageHashstringEvent message hash (empty string until completed)
transactionHashstring | nullDelivery transaction hash (null until completed)

proofPayload — data for non-credit transfers

FieldTypeDescription
txBlockProofstringBlock proof (BOC base64)
txProofstringTransaction Merkle proof (BOC base64)
messageHashstringEvent message hash
outMessageIndexnumberIndex of outgoing message in transaction
eventobjectEvent data (see below)
abiTxobject | nullReady transaction for deployEvent (see below)

proofPayload.abiTx — ready transaction for non-credit

FieldTypeDescription
txstringTransaction payload (BOC base64)
executionAddressstringEventConfiguration contract address
attachedValuestringRequired gas in nano-units
abiMethodstringContract method (deployEvent)
abistringContract ABI
paramsstringCall parameters (JSON string)

proofPayload.event — event data

FieldTypeDescription
tokenTypestring"Native" or "Alien"
chainIdnumberDestination network Chain ID
tokenstringToken address
amountstringAmount in nano-units
recipientstringRecipient address
valuestringAttached value (gas)
expectedGasstringExpected gas
remainingGasTostringAddress for gas return
senderstringSender address
payloadstringAdditional payload (BOC base64)
nativeProxyWalletstring | nullProxy wallet address (for Native)
namestring | nullToken name
symbolstring | nullToken symbol
decimalsnumber | nullNumber of decimals

notInstantTransfer — liquidity request data

Not applicable for TVM↔TVM

The notInstantTransfer field is only populated for EVM-related transfers (TvmEvm, EvmEvm, TvmEvmTvm). For TVM↔TVM transfers this field is always null. Limits and insufficient liquidity are handled at the Event contract level — see Handling Limits and Liquidity Requests.

Response Interpretation

FieldValueAction
transferStatus: "Pending"WaitingContinue polling
transferStatus: "Completed"CompletedSuccess!
transferStatus: "Failed"ErrorHandle error
proofPayload != nullProof readyCan deploy event (non-credit)
incoming.transactionHash != nullSecond part executedTransfer almost completed

Transfer History

API Endpoint

POST https://tetra-history-api.chainconnect.com/v2/transfers/search
Content-Type: application/json

Request Parameters

ParameterTypeRequiredDescription
userAddressesstring[]Filter by user addresses (max 7)
transferKindsstring[]Transfer type: ["TvmToTvm"]
statusesstring[]Filter by statuses: ["Pending", "Completed", "Failed"]
fromTvmChainIdnumberSource network Chain ID
toTvmChainIdnumberDestination network Chain ID
createdAtGenumberUnix timestamp >= (from date)
createdAtLenumberUnix timestamp <= (to date)
orderingstringSorting: "CreatedAtAscending" or "CreatedAtDescending"
limitnumberRecord limit (max count in response)
offsetnumberOffset (for pagination)
isNeedTotalCountbooleanWhether to return total transfer count

Request Example

bash
curl -X POST 'https://tetra-history-api.chainconnect.com/v2/transfers/search' \
  -H 'Content-Type: application/json' \
  -d '{
    "userAddresses": ["0:3333...cccc"],
    "transferKinds": ["TvmToTvm"],
    "statuses": ["Pending", "Completed"],
    "fromTvmChainId": -239,
    "toTvmChainId": 2000,
    "ordering": "CreatedAtDescending",
    "limit": 10,
    "offset": 0,
    "isNeedTotalCount": true
  }'

Response Example

json
{
  "transfers": [
    {
      "tvmTvm": {
        "transferStatus": "Completed",
        "timestampCreatedAt": 1767972717,
        "outgoing": {
          "tokenType": "Native",
          "chainId": -239,
          "userAddress": "0:3333...cccc",
          "tokenAddress": "0:1111...aaaa",
          "proxyAddress": "0:6666...ffff",
          "volumeExec": "1.0000",
          "transactionHash": "abc123..."
        },
        "incoming": {
          "tokenType": "Alien",
          "chainId": 2000,
          "userAddress": "0:2222...bbbb",
          "tokenAddress": "0:4444...dddd",
          "proxyAddress": "0:7777...0000",
          "volumeExec": "0.9990",
          "transactionHash": "def456..."
        }
      }
    }
  ],
  "totalCount": 42
}

Transfer ID for each transfer: transfers[i].tvmTvm.outgoing.transactionHash

Tracking Algorithm

┌─────────────────────────────────────────────────────────────────────────┐
│  1. GET TRANSFER ID                                                     │
│     After sending tokens to the Proxy contract, save the transaction    │
│     hash of that transaction — this is the Transfer ID                  │
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│  2. REQUEST STATUS                                                      │
│     POST /v2/transfers/status with Transfer ID and Chain ID             │
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│  3. CHECK transferStatus                                                │
│                                                                         │
│     "Pending"   → Transfer in progress, retry request in 5-10 sec       │
│     "Completed" → Success! Tokens delivered to recipient                │
│     "Failed"    → Error, transfer rejected                              │
└─────────────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────┐
│  4. FOR COMPLETED TRANSFER                                              │
│     incoming.transactionHash — hash of token delivery transaction       │
│     incoming.volumeExec — received amount (after fees)                  │
└─────────────────────────────────────────────────────────────────────────┘

What to Check in Response

FieldMeaning
transferStatus: "Pending"Transfer in progress — retry request
transferStatus: "Completed"Transfer completed — tokens delivered
transferStatus: "Failed"Transfer rejected — handle error
incoming.transactionHash != nullDelivery transaction executed
proofPayload != nullMerkle proof ready (for non-credit transfers)

Credit vs Non-credit Transfers

TypeDescriptionUser Actions
CreditRelays automatically deploy event and deliver tokensOnly track status
Non-creditUser deploys event contract themselvesWait for proofPayload, deploy event

For non-credit transfers:

  1. Wait for proofPayload to appear in response
  2. Use the ready transaction from proofPayload.abiTx to deploy the Event contract in the destination network (see Sending a Transfer)
  3. After deploying Event contract, the transfer will complete automatically

Getting Status from Contract (on-chain)

In addition to Bridge Aggregator API, transfer status can be obtained directly from the Event contract in the blockchain. This is useful for:

  • Verifying API data
  • Working without dependency on centralized API
  • Getting detailed on-chain status

Event Contract Address

proofPayload.abiTx.executionAddress is the EventConfiguration address, not the Event contract address. The Event contract is deployed when deployEvent is called and its address is computed deterministically via deriveEventAddress(msgHash) on EventConfiguration. To get the Event contract address, use proofPayload.messageHash and call deriveEventAddress on EventConfiguration.

Event Contract Statuses

typescript
const EVENT_STATUS = {
  0: 'Initializing',
  1: 'Pending',
  2: 'Confirmed',
  3: 'Rejected',
  4: 'Cancelled',
  5: 'LimitReached',
  6: 'LiquidityRequested',
  7: 'LiquidityProvided',
  8: 'Verified',
} as const;

Getting Status via getDetails

The Event contract has a getDetails() method that returns:

FieldTypeDescription
_eventInitDatatupleInitialization data: msgHash (uint256), configuration (address), chainId (int32)
_initializeraddressAccount that deployed the Event contract
_metacellMetadata from EventConfiguration
_statusuint8Numeric status (see table above)

Reading on-chain data

Reading contract state requires a TVM provider (everscale-inpage-provider). TonConnect only supports sending transactions.

typescript
import { ProviderRpcClient, Address } from 'everscale-inpage-provider';

const provider = new ProviderRpcClient();
await provider.ensureInitialized();

const eventAbi = {
  "ABI version": 2,
  functions: [{
    name: "getDetails",
    inputs: [{ name: "answerId", type: "uint32" }],
    outputs: [
      { name: "_eventInitData", type: "tuple", components: [
        { name: "msgHash", type: "uint256" },
        { name: "configuration", type: "address" },
        { name: "chainId", type: "int32" },
      ]},
      { name: "_initializer", type: "address" },
      { name: "_meta", type: "cell" },
      { name: "_status", type: "uint8" },
    ],
  }],
  events: [],
} as const;

const eventAddress = new Address('0:1234...abcd'); // Event contract address
const eventContract = new provider.Contract(eventAbi, eventAddress);

const { _status, _initializer, _eventInitData } = await eventContract.methods
  .getDetails({ answerId: 0 })
  .call();

const statusName = EVENT_STATUS[Number(_status) as keyof typeof EVENT_STATUS];
console.log('Event status:', statusName); // e.g.: "Verified"

Getting Event Data via getDecodedData

The getDecodedData() method returns decoded event data, including the bounty (reward for LP). Uses the same provider and eventAddress from the example above.

Different Return Types

The response structure differs for Native and Alien Event contracts. Use the appropriate ABI.

typescript
const nativeEventAbi = {
  "ABI version": 2,
  functions: [{
    name: "getDecodedData",
    inputs: [{ name: "answerId", type: "uint32" }],
    outputs: [
      { name: "token_", type: "address" },
      { name: "name_", type: "string" },
      { name: "symbol_", type: "string" },
      { name: "decimals_", type: "uint8" },
      { name: "sender_", type: "address" },
      { name: "amount_", type: "uint128" },
      { name: "recipient_", type: "address" },
      { name: "value_", type: "uint128" },
      { name: "expected_gas_", type: "uint128" },
      { name: "payload_", type: "optional(cell)" },
      { name: "proxy_", type: "address" },
      { name: "tokenWallet_", type: "address" },
      { name: "bounty_", type: "uint128" },
    ],
  }],
  events: [],
} as const;

const eventContract = new provider.Contract(nativeEventAbi, eventAddress);
const data = await eventContract.methods.getDecodedData({ answerId: 0 }).call();

console.log('Token:', data.symbol_);
console.log('Amount:', data.amount_);
console.log('Recipient:', data.recipient_.toString());
console.log('Bounty:', data.bounty_);
typescript
const alienEventAbi = {
  "ABI version": 2,
  functions: [{
    name: "getDecodedData",
    inputs: [{ name: "answerId", type: "uint32" }],
    outputs: [
      { name: "base_chainId_", type: "int32" },
      { name: "base_token_", type: "address" },
      { name: "base_native_proxy_wallet_", type: "address" },
      { name: "name_", type: "string" },
      { name: "symbol_", type: "string" },
      { name: "decimals_", type: "uint8" },
      { name: "amount_", type: "uint128" },
      { name: "recipient_", type: "address" },
      { name: "attached_gas_", type: "uint128" },
      { name: "expected_gas_", type: "uint128" },
      { name: "payload_", type: "optional(cell)" },
      { name: "proxy_", type: "address" },
      { name: "token_", type: "address" },
      { name: "native_proxy_token_wallet", type: "optional(address)" },
    ],
  }],
  events: [],
} as const;

const eventContract = new provider.Contract(alienEventAbi, eventAddress);
const data = await eventContract.methods.getDecodedData({ answerId: 0 }).call();

console.log('Token:', data.symbol_);
console.log('Amount:', data.amount_);
console.log('Recipient:', data.recipient_.toString());
console.log('Base chain:', data.base_chainId_);

Handling Limits and Liquidity Requests

Sometimes after deploying the Event contract, tokens are not delivered immediately. This happens in two cases:

SituationEvent StatusWhat HappenedWhich Tokens
Limit exceededLimitReached (5)Amount exceeds daily security limitNative and Alien
Insufficient liquidityLiquidityRequested (6)Proxy contract doesn't have enough tokens to unlockNative in destination network only

In both cases, tokens are not lost — they are locked in the Event contract until the situation is resolved.

Why Alien tokens don't hit LiquidityRequested?

Alien tokens (not native to the destination network) are minted (created) during transfer, not taken from the proxy balance. Therefore, insufficient liquidity is impossible for them. Native tokens (native to the destination network) are unlocked from the proxy — if the proxy doesn't hold enough locked tokens, LiquidityRequested occurs.

How to Determine if a Transfer is Delayed

Via API: transferStatus remains "Pending", while the on-chain Event contract status is 5 (LimitReached) or 6 (LiquidityRequested).

Via on-chain: call getDetails() on the Event contract:

typescript
const { _status } = await eventContract.methods.getDetails({ answerId: 0 }).call();

if (Number(_status) === 5) {
  console.log('Daily limit exceeded — awaiting approval');
} else if (Number(_status) === 6) {
  console.log('Insufficient liquidity — can set bounty, cancel, or retry');
}

User Actions

ActionWhen AvailableWho Can CallDescription
Set BountyLiquidityRequested (6)sender / recipientSet a reward for liquidity provider
CancelLiquidityRequested (6)sender / recipientCancel transfer and return tokens
CancelLimitReached (5)limitApprover onlyReject the transfer (administrator)
RetryLimitReached (5) / LiquidityRequested (6)sender / recipient / initializerRetry after limit approval or liquidity becomes available

Set Bounty — Setting a Reward

Sets a reward for the liquidity provider. Available only in LiquidityRequested (6) status. Can be called by sender or recipient.

typescript
import { ProviderRpcClient, Address } from 'everscale-inpage-provider';

const provider = new ProviderRpcClient();
await provider.ensureInitialized();

const eventAbi = {
  "ABI version": 2,
  functions: [{
    name: "setBounty",
    inputs: [{ name: "_bounty", type: "uint128" }],
    outputs: [],
  }],
  events: [],
} as const;

const eventAddress = new Address('0:1234...abcd'); // Event contract address
const eventContract = new provider.Contract(eventAbi, eventAddress);

const senderAddress = new Address('0:3333...cccc'); // sender or recipient

await eventContract.methods
  .setBounty({ _bounty: '1000000000' }) // 1 token in nano-units
  .send({
    from: senderAddress,
    amount: '500000000', // 0.5 for gas
    bounce: true,
  });

console.log('Bounty set');

Cancel — Cancelling a Transfer

Cancels the transfer and returns tokens to the specified address. Available in LiquidityRequested (6) status for sender/recipient, and in LimitReached (5) for limitApprover.

Different Parameters for Native and Alien

Alien Event contract requires an additional _predeployedTokenData parameter. Not needed for Native.

typescript
import { ProviderRpcClient, Address } from 'everscale-inpage-provider';

const provider = new ProviderRpcClient();
await provider.ensureInitialized();

const nativeEventAbi = {
  "ABI version": 2,
  functions: [{
    name: "cancel",
    inputs: [
      { name: "_newRecipient", type: "address" },
      { name: "_remainingGasTo", type: "address" },
      { name: "_expectedGas", type: "uint128" },
      { name: "_eventPayload", type: "optional(cell)" },
      { name: "_expectedGasReceiver", type: "address" },
    ],
    outputs: [],
  }],
  events: [],
} as const;

const eventAddress = new Address('0:1234...abcd');
const eventContract = new provider.Contract(nativeEventAbi, eventAddress);

const senderAddress = new Address('0:3333...cccc');

await eventContract.methods
  .cancel({
    _newRecipient: senderAddress,         // Where to return tokens
    _remainingGasTo: senderAddress,       // Where to return remaining gas
    _expectedGas: '0',
    _eventPayload: null,                  // No additional payload
    _expectedGasReceiver: senderAddress,
  })
  .send({
    from: senderAddress,
    amount: '1000000000', // 1.0 for gas
    bounce: true,
  });

console.log('Transfer cancelled');
typescript
import { ProviderRpcClient, Address } from 'everscale-inpage-provider';

const provider = new ProviderRpcClient();
await provider.ensureInitialized();

const alienEventAbi = {
  "ABI version": 2,
  functions: [{
    name: "cancel",
    inputs: [
      { name: "_newRecipient", type: "address" },
      { name: "_remainingGasTo", type: "address" },
      { name: "_expectedGas", type: "uint128" },
      { name: "_eventPayload", type: "optional(cell)" },
      { name: "_predeployedTokenData", type: "optional(cell)" },
      { name: "_expectedGasReceiver", type: "address" },
    ],
    outputs: [],
  }],
  events: [],
} as const;

const eventAddress = new Address('0:1234...abcd');
const eventContract = new provider.Contract(alienEventAbi, eventAddress);

const senderAddress = new Address('0:3333...cccc');

await eventContract.methods
  .cancel({
    _newRecipient: senderAddress,
    _remainingGasTo: senderAddress,
    _expectedGas: '0',
    _eventPayload: null,
    _predeployedTokenData: null,
    _expectedGasReceiver: senderAddress,
  })
  .send({
    from: senderAddress,
    amount: '1000000000',
    bounce: true,
  });

console.log('Transfer cancelled');

Retry — Retrying Delivery

Retries token delivery. Use after:

  • Limit approval by administrator (from LimitReached status)
  • Liquidity becomes available on proxy (from LiquidityRequested status)
typescript
import { ProviderRpcClient, Address } from 'everscale-inpage-provider';

const provider = new ProviderRpcClient();
await provider.ensureInitialized();

const eventAbi = {
  "ABI version": 2,
  functions: [{
    name: "retry",
    inputs: [],
    outputs: [],
  }],
  events: [],
} as const;

const eventAddress = new Address('0:1234...abcd');
const eventContract = new provider.Contract(eventAbi, eventAddress);

const senderAddress = new Address('0:3333...cccc');

await eventContract.methods
  .retry({})
  .send({
    from: senderAddress,
    amount: '500000000', // 0.5 for gas
    bounce: true,
  });

console.log('Retry sent — transfer will move to Verified status');

ChainConnect Bridge Documentation