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",
        "contractAddress": "aaaa...1111",
        "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": null,
        "contractAddress": null,
        "chainId": 2000,
        "userAddress": "0:2222...bbbb",
        "tokenAddress": null,
        "proxyAddress": null,
        "volumeExec": null,
        "volumeUsdtExec": null,
        "feeVolumeExec": null,
        "messageHash": null,
        "transactionHash": null
      }
    }
  },
  "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
    },
    "feeAmount": "1000"
  }
}

Response Field Descriptions

transfer.tvmTvm — transfer data

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

outgoing / incoming — transfer side data

FieldTypeDescription
tokenTypestring | null"Native" or "Alien"
contractAddressstring | nullEvent contract address (hex without 0:)
chainIdnumberNetwork Chain ID
userAddressstringUser address (sender/recipient)
tokenAddressstring | nullToken address in this network
proxyAddressstring | nullProxy contract address
volumeExecstring | nullAmount in human-readable format
volumeUsdtExecstring | nullAmount in USDT equivalent
feeVolumeExecstring | nullFee charged
messageHashstring | nullEvent message hash
transactionHashstring | nullTransaction hash (Transfer ID for outgoing)

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)
feeAmountstring | nullFee in nano-units

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)
namestringToken name
symbolstringToken symbol
decimalsnumberNumber of decimals

notInstantTransfer — liquidity request data

Populated when the transfer requires liquidity (LimitReachedLiquidityRequested status).

FieldTypeDescription
liquidityRequestAddressstringLiquidity request contract address
statusstringRequest status
amountstringRequested amount

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",
          "contractAddress": "aaaa...1111",
          "chainId": -239,
          "userAddress": "0:3333...cccc",
          "tokenAddress": "0:1111...aaaa",
          "proxyAddress": "0:6666...ffff",
          "volumeExec": "1.0000",
          "transactionHash": "abc123..."
        },
        "incoming": {
          "tokenType": "Alien",
          "contractAddress": "bbbb...2222",
          "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 proofPayload.txBlockProof and proofPayload.txProof to deploy Event contract in destination network
  3. After deploying Event contract, the transfer will complete automatically

Example: TypeScript polling

typescript
async function waitForTransferCompletion(
  transferId: string,
  chainId: number,
  maxAttempts = 60,
  initialDelayMs = 5000
): Promise<TransferStatusResponse> {
  let delay = initialDelayMs;

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const response = await fetch('https://tetra-history-api.chainconnect.com/v2/transfers/status', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        tvmTvm: {
          outgoingTransactionHash: transferId,
          dappChainId: chainId,
        },
      }),
    });

    const status = await response.json();

    if (status.transfer.tvmTvm?.transferStatus === 'Completed') {
      console.log(`Transfer completed after ${attempt} attempts`);
      return status;
    }

    if (status.transfer.tvmTvm?.transferStatus === 'Failed') {
      throw new Error('Transfer failed');
    }

    console.log(`Attempt ${attempt}: status = ${status.transfer.tvmTvm?.transferStatus}`);
    await new Promise(resolve => setTimeout(resolve, delay));
    delay = Math.min(delay * 1.2, 30000); // Max 30 seconds
  }

  throw new Error('Transfer timeout');
}

// Usage
const transferId = 'abc123def456...';  // Transfer ID from Step 3 of sending a transfer
const result = await waitForTransferCompletion(transferId, -239);
console.log('Transfer completed:', result.transfer.tvmTvm?.incoming);

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

Event contract address can be obtained from API response:

  • transfer.tvmTvm.outgoing.contractAddress — hex address without 0: prefix

Reading Status from Contract

typescript
import { TonClient, Address } from '@ton/ton';

// ABI for getDetails method of Event contract
const EVENT_CONTRACT_ABI = {
  getDetails: {
    name: 'getDetails',
    inputs: [],
    outputs: [
      { name: 'status', type: 'uint8' },
      { name: 'sender', type: 'address' },
      { name: 'recipient', type: 'address' },
      { name: 'amount', type: 'uint128' },
      // ... other fields
    ],
  },
};

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

async function getEventContractStatus(
  client: TonClient,
  eventContractAddress: string
): Promise<{ status: string; statusCode: number }> {
  const address = Address.parse(`0:${eventContractAddress}`);

  // Call contract get-method
  const result = await client.runMethod(address, 'getDetails');

  // Parse result
  const statusCode = result.stack.readNumber();
  const status = EVENT_STATUS[statusCode as keyof typeof EVENT_STATUS] || 'Unknown';

  return { status, statusCode };
}

// Usage
const client = new TonClient({
  endpoint: 'https://toncenter.com/api/v2/jsonRPC',
});

// contractAddress from API response (without 0:)
const eventContractAddress = 'aaaa...1111';
const { status, statusCode } = await getEventContractStatus(client, eventContractAddress);

console.log(`Event status: ${status} (code: ${statusCode})`);

Full Event Contract Data

To get all event data, use the getDetails method:

typescript
interface EventDetails {
  status: number;
  sender: string;
  recipient: string;
  amount: bigint;
  token: string;
  chainId: number;
  // ... other fields depend on event type
}

async function getFullEventDetails(
  client: TonClient,
  eventContractAddress: string
): Promise<EventDetails> {
  const address = Address.parse(`0:${eventContractAddress}`);
  const result = await client.runMethod(address, 'getDetails');

  return {
    status: result.stack.readNumber(),
    sender: result.stack.readAddress().toString(),
    recipient: result.stack.readAddress().toString(),
    amount: result.stack.readBigNumber(),
    token: result.stack.readAddress().toString(),
    chainId: result.stack.readNumber(),
  };
}

Important

The getDetails response structure may differ for different Event contract versions (Native vs Alien). Check the contract ABI before parsing.

ChainConnect Bridge Documentation