Wallet API
Building an Exchange Wallet

Building an Exchange Wallet#

Step 1: Create a Project and Configure#

Before starting, some preparation work is required:

Create a Project and API Key#

Before using the wallet API, create a project and generate an API key on the developer portal:

  1. Log in to the Developer Portal.
  2. Create a new project.
  3. Generate an API key in the project settings.

Next, some configuration work is needed to be done by the developer.

REST Request Authentication Configuration#

When sending REST requests, authentication is required. Please refer to the REST Request Authentication Guide.

Node.js Environment Setup#

Import the necessary Node.js libraries and set your environment variables Node.js Environment Setup.


Step 2: Generate Wallet Private Key and Address#

You can use our Signing SDK to create wallet mnemonics and addresses.

npm install#

First, you need to install the latest version of the Signing SDK,take EVM network as an example:

npm install @okxweb3/crypto-lib
npm install @okxweb3/coin-base
npm install @okxweb3/coin-ethereum

Local Build#

  1. Download the project source code
git clone https://github.com/okx/js-wallet-sdk.git
  1. Run the build script
sh build.sh

Taking the ETH network as an example, you can use the Signing SDK to create an ETH wallet object and derive the corresponding address.

import { bip39, BigNumber } from "@okxweb3/crypto-lib";
import { EthWallet } from "@okxweb3/coin-ethereum";

// eth wallet
let wallet = new EthWallet();

// get mnemonic
let mnemonic = await bip39.generateMnemonic();
console.log("generate mnemonic:", mnemonic);

// get derived key
const hdPath = await wallet.getDerivedPath({ index: 0 });
let derivePrivateKey = await wallet.getDerivedPrivateKey({ mnemonic: mnemonic, hdPath: hdPath });
console.log("generate derived private key:", derivePrivateKey, ",derived path: ", hdPath);

// get new address
let newAddress = await wallet.getNewAddress({ privateKey: derivePrivateKey });
console.log("generate new address:", newAddress.address);

// get the public key
let publicKey = newAddress.publicKey;
console.log("the corresponding public key:", publicKey);

Demo Program#

We have prepared an open-source demo program to demonstrate the features of the Signing SDK mentioned above. Get the demo program source code here.


Step 3: Create an Account#

You have already generated the mnemonic and address in the previous step. Now you can create an account (AccountId) to aggregate multiple addresses for batch querying of token balances and transaction history.

Generate UNIX Timestamp Message Signature#

let now = new Date();

let timestamp = now.getTime();

let timestampString = timestamp.toString();

console.log(timestampString);

let signParams = {
    privateKey: derivePrivateKey,
    data: timestampString
}

let signature = await wallet.signMessage(signParams);
console.log(signature);

Create a Wallet Account#

Using the obtained address, publicKey, signature, signMessage, and chainIndex information, call the POST /api/v5/waas/account/create-account interface to create a wallet account.

For example, for exchange scenarios, subscribing to multiple addresses on the same chain is implemented as follows:

// Define your parameters
const addresses = [
    {
        "chainIndex":"1",
        "address":"0x68800215a2c08e5e65ff20ac0289566a42e2c18e",
        "publicKey":"0xf30bae03d162d5ac2cce4678a244fa6c12ea20ffe1e32881e9e3fb016f3f8aeffe794c7173a8fd3fcff573736241681335bd779554961d729a93ce7efc8683c9",
        "signature":"0xa3d53079890cb07a5fc2dac4afb5d183f2e5a45c2953f6676bc9fb84a7e4602747401e5904fce4470399e54db6dc437688a0bbe3943cbc53f15197db7f3dc1b81b"
    },
    {
        "chainIndex":"1",
        "address":"0x4c8f8089607ba1d370c7c62743a309bcb01d1eec",
        "publicKey":"0xcbc322ae2148d436630e8884d927a13424075162580298f03c4664128122e982ca2d4b7f89bd5663cb41f2a6535b35adc384681ad2914e16eb413a046a39dba3",
        "signature":"0xf189f73d8136c9d44cde11a4a55d3c5bb9817cfc9b7edf82e2ae69195ba986ee7d679d7a8ca9bd12f28545429cadc4ed7045dba9d478fcbc0fd28f232db3841e1b"
    }
];

const getCreateAccountBody = {
    addresses: addresses,
    signMessage: '1718798213000', // UNIX Timestamp in milliseconds
};

// Define auxiliary function
const getCreateAccountData = async () => {
    const apiRequestUrl = getRequestUrl(
        apiBaseUrl,
        '/api/v5/waas/account/create-account'
    );
    return fetch(apiRequestUrl, {
        method: 'post',
        headers: headersParams,
        body: JSON.stringify(getCreateAccountBody),
    })
    .then((res) => res.json())
    .then((res) => {
        return res;
    });
  };

const { data: createAccountData } = await getCreateAccountData();

Step 4: Compose and Send Transactions#

Withdrawals can be achieved in three steps:

Obtain Signature Information#

First, call the POST /api/v5/waas/wallet/pre-transaction/sign-info interface to query the data required for transaction, such as gas price, gas limit, nonce. Take ETH for an example:

// Define your parameters
const postSignInfoBody = {
    chainIndex: '1',
    fromAddr: '0xdf54b6c6195ea4d948d03bfd818d365cf175cfc2',
    toAddr: '0x1e80c39051f078ee34763282cbb36ffd88b40c65',
    txAmount: '123000000000000',
    extJson: {
        inputData: '041bbc6fa102394773c6d8f6d634320773af4'
    }
  };

// Define auxiliary function
const signInfoData = async () => {
    const apiRequestUrl = getRequestUrl(
        apiBaseUrl,
        '/api/v5/waas/wallet/pre-transaction/sign-info'
    );
    return fetch(apiRequestUrl, {
        method: 'post',
        headers: headersParams,
        body: JSON.stringify(postSignInfoBody),
    })
    .then((res) => res.json())
    .then((res) => {
        return res;
    });
  };

const { data: signInfoData } = await signInfoData();

When the relevant information is queried, you will receive a response like this:

{
    "code": 0,
    "msg": "success",
    "data": [
        {
            "gasLimit": "21000",
            "nonce": "0",
            "gasPrice": {
                "normal": "12200500000",
                "min": "9130000000",
                "max": "16801000000",
                "supportEip1559": true,
                "erc1599Protocol": {
                    "suggestBaseFee": "8630000000",
                    "proposePriorityFee": "550000000",
                    "safePriorityFee": "500000000",
                    "fastPriorityFee": "2130000000",
                    "baseFee": "8630000000"
                }
            }
        },
    ]
}

Verify Address and Build Transaction#

Use the Signing SDK to verify the address and build the transaction.

// Verify address
let valid = await wallet.validAddress({
    address: newAddress.address
});
console.log("verify address isValid:", valid.isValid);

// Sign the transaction
let signParams = {
    privateKey: derivePrivateKey,
    data: {
        to: newAddress.address,
        value: new BigNumber(0),
        nonce: 5,
        gasPrice: new BigNumber(100 * 1000000000),
        gasLimit: new BigNumber(21000),
        chainId: 42
    }
};

let signedTx = await wallet.signTransaction(signParams);
console.log("signed tx:", signedTx);

Broadcast Transaction#

Call the POST /api/v5/waas/wallet/pre-transaction/send-transaction interface to broadcast the transaction to the chain.

// Define your parameters
const postSendTransactionBody = {
    signedTx: "0xf8aa6a84ee6b280083012e389455d398326f99059ff775485246999027b319795580b844a9059cbb000000000000000000000000e0f7a45f1aa6cfff6fbbb1049f7a8c9248312c2e00000000000000000000000000000000000000000000000000000002540be4008193a0dfecbffac07db267e7438fe94f48c0d30bb17c3d86774bd445a9d70d06a974e9a0363c583a80dfc099d3a4186ce943d4bb38420dd52e23d26d30a73bfa8e291ed9",
    chainId: "1",
    accountId:"13886e05-1265-4b79-8ac3-b7ab46211001",
    fromAddr: "0x238193be9e80e68eace3588b45d8cf4a7eae0fa3",
    toAddr: "0xe0f7a45f1aa6cfff6fbbb1049f7a8c9248312c2e",
    txHash: "0x585efb06371c2827a8da7b3dc2f0d3daf4f61cd14959c928bc698c43822881c3",
    txAmount: "10000000000",
    serviceCharge: "4128241194000",
    tokenAddress: "0x55d398326f99059ff775485246999027b3197955",
    txType: "transfer",
    extJson: {
        gasPrice: "196582914",
        gasLimit: "21000",
        nonce: "578"
    }
};

// Define auxiliary function
const sendTransactionData = async () => {
    const apiRequestUrl = getRequestUrl(
      apiBaseUrl,
      'api/v5/waas/wallet/pre-transaction/send-transaction'
    );
    return fetch(apiRequestUrl, {
      method: 'post',
      headers: headersParams,
      body: JSON.stringify(postSendTransactionBody),
    })
    .then((res) => res.json())
    .then((res) => {
        return res;
    });
};

const { data: sendTransactionData } = await sendTransactionData();

After broadcasting, you will receive a response like this:

{
    "code": 0,
    "msg": "success",
    "data": [{
        "orderId": "469356614923743232"
    }]
}
Note
Click to view the definition of orderId.

Step 5: Query Transaction Details#

After sending the transaction, use the GET /api/v5/waas/wallet/post-transaction/transaction-detail-by-ordid interface and the orderId to get the transaction details. (You can also query by transaction hash, see API Reference).

// Define your parameters
const params = {
    accountId: '13886e05-1265-4b79-8ac3-b7ab46211001',
    orderId: '469356614923743232',
    chainIndex: '1'
};

// Define auxiliary function
const getTransactionDetailData = async () => {
    const apiRequestUrl = getRequestUrl(
      apiBaseUrl,
      '/api/v5/waas/wallet/post-transaction/transaction-detail-by-ordid',
      params
    );
    return fetch(apiRequestUrl, {
      method: 'get',
      headers: headersParams,
    })
    .then((res) => res.json())
    .then((res) => {
        return res;
    });
};

const { data: transactionDetailData } = await getTransactionDetailData();

When the details are successfully queried, you will receive a response like this:

{
    "code": 0,
    "msg": "success",
    "data": [
        {
            "chainIndex": "1",
            "orderId": "469356614923743232",
            "txhash": "0xcbf411766d65f3cf92839ababa73c4afec69a83442e8b67a68b5104b50a04ejb",
            "blockHash" : "0x3ee63382b883fc40e35da6023fb341dd01cd2ec011f992bb54cf312f517740c9",
            "blockHeight" : "19235519",
            "blockTime" : "1708026551000",
            "feeUsdValue" : "138.846919",
            "fees" : "49102595635945621",
            "gasLimit" : "2000000",
            "gasPrice" : "87504603347",
            "gasUsed" : "561143",
            "txType": "0",  
            "txTime": "0", 
            "txStatus": "0",
            "txDetail": [
                {
                    "iType" : "0",  
                    "txAmount": "0",
                    "tokenAddress":""
                    "contractCall" : true,
                    "fromAddr" : "0x3ee63382b883fc40e35da6023fb341dd01cd2ec011f992bb54cf312f517740c9",
                    "toAddr" : "0x3ee63382b883fc40e35da6023fb341dd01cd2ec011f992bb54cf312f517740c9",
                    "logIndex" : "-1",
                    "status" : "SUCCESS"
                }
            ]
        }
    ]
}

Additionally, you can use Webhook Subscription to get real-time and accurate information about the status of withdrawal transactions. This feature can also be used to discover deposit transactions in real-time.


Step 6: Add Token and Query Balance#

Wallet accounts support aggregate balance queries for millions of addresses, which can be used for exchange reconciliation scenarios.

Before querying the token balance using a wallet account, you need to call /api/v5/waas/wallet/asset/add-token to add the token of interest.

// Define parameters
const postAddTokenBody = {
    accountId: '13886e05-1265-4b79-8ac3-b7ab46211001',
    chainIndex: '1',
    tokenAddress: '0xdac17f958d2ee523a2206206994597c13d831ec7'
};

// Define auxiliary function
const postAddTokenData = async () => {
    const apiRequestUrl = getRequestUrl(
        apiBaseUrl,
        '/api/v5/waas/wallet/asset/add-token'
    );
    return fetch(apiRequestUrl, {
        method: 'post',
        headers: headersParams,
        body: JSON.stringify(postAddTokenBody),
    })
    .then((res) => res.json())
    .then((res) => {
        return res;
    });
};

const { data: addTokenData } = await postAddTokenData();

After successfully subscribing, you can call the POST /api/v5/waas/wallet/asset/token-balances interface to view the total balance of the token held by all addresses under the account.

// Define parameters
const getAssetsParams = {
    accountId: '13886e05-1265-4b79-8ac3-b7ab46211001',
    tokenAddresses: [
        {
            "chainIndex": "1",
            "tokenAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7"
        },
    ]
};

// Define auxiliary function
const getAssetsData = async () => {
    const apiRequestUrl = getRequestUrl(
      apiBaseUrl,
      '/api/v5/waas/asset/token-balances',
      getAssetsParams
    );
    return fetch(apiRequestUrl, {
      method: 'GET',
      headers: headersParams,
    })
    .then((res) => res.json())
    .then((res) => {
        return res;
    });
  };

const { data: getAssetsData } = await getAssetsData();

When assets are queried, you will receive a response like this:

{
    "code": "0",
    "data": [
        {
            "chainIndex": "1",
            "tokenAddress": "0xdac17f958d2ee523a2206206994597c13d831ec7",
            "symbol": "USDT",
            "balance": "0.688467",
            "tokenPrice": "0.99977",
            "tokenType": "1"
        }
    ],
    "msg": "success"
}

At this point, you have basically implemented the basic functions needed to develop an exchange wallet.

Additionally, OnchainOS provides a rich range of interfaces to fully meet the needs of exchange wallet developers. For details, please refer to the API Reference.