これは XYM
トークンを 交換所プラットフォーム への統合を通じて 開発者をガイドする ことを目的とした資料です。アカウントの設定、預け入れ資産の監視、引き出しの作成方法に関する推奨事項と、すぐに利用できるコード例を含みます。
共有しているコード例は Symbol SDK TypeScript版 ですが、他の 有効な SDK にも、それらが同じ設計原則を共有しているため、移植することができます。必要なプログラミング言語で SDK がサポートされていない場合は REST API を介して、直接接続して統合できる可能性があります。
交換所を設計する方法はたくさんあります。このガイドは 中央ウォレットアプローチ に基づいた、交換所での XYM
の入出金をサポートする方法に基づいています。
このデザインは他のデザインと比べると、特に推奨はできません。ただし、その 簡素なアーキテクチャ は、交換所との統合に関する Symbol の一連の機能の優れたショーケースといえます。別のアプローチでは、例えば、ユーザーごとに異なるウォレットを使用するなどです。
次に、このアーキテクチャの主なコンポーネントについて説明します。
交換所はすべてのユーザーの入出金を扱う Symbol アカウントを所有します。このアカウントのキーはオンラインマシン上に存在する必要があるため、これは一般に ホット ウォレットとも呼ばれます。このアカウントは攻撃に最も晒されるアカウントであるため、日常の使用 (引き出しと預け入れ) に必要な量の XYM しかもちません。
Cold wallet(s) hold a certain threshold for the pool of XYM. These accounts should be created and remain in a setup with no internet connection. Transactions issued from cold wallets must be signed offline and announced to the network using another device. It is advisable as well that cold wallets are set up with multisig accounts.
In the proposed architecture, each user is identified by a Unique User IDentifier (UUID) on the exchange's database. A user will deposit to the central wallet with their UUID attached as the message of the transaction (called sometimes the memo). The UUID is only shown in the user's dashboard during the deposit confirmation.
One of the drawbacks of this design is that many users are not used to having a message attached to their transactions. If they forget to attach the UUID or attach a wrong UUID, it will lead to receiving lots of support tickets concerning "lost funds".
注意
Symbol の 転送トランザクション は 1023 バイトまでのメッセージを添付することができますが、 Symbol SDK は 先頭バイトを特別な処理のために 使用します。
This can be a source of confusion because the receiver of a transaction does not know if the message was generated by the Symbol SDK or otherwise (for example accessing the REST gateway), so it does not know if the first byte must be treated specially or not.
To avoid any issue, the following measures must always be enforced:
Always start messages with a byte in the 32 to 128 range (this is the standard ASCII printable range).
Always ignore any received initial byte outside the 32 to 128 range.
Follow these rules, regardless of whether you use the Symbol SDK or not to generate and parse transfer transactions.
Although not absolutely necessary, it is recommended that Exchanges deploy their own Symbol node to communicate with the rest of the network. Since each node automatically connects to several other nodes on the network, this approach is more robust than accessing the network always through the same public node, which might become unavailable.
If you are unable to run your own node, you can choose one from the list provided by the Statistics Service.
See the different guides about deploying Symbol nodes and make sure you create an API node.
Exchanges can create the central and cold wallets by downloading the official Symbol Desktop Wallet for Windows, Linux or Mac.
Every wallet has assigned an account (a deposit box that holds tokens, which can only be transferred with the appropriate private key).
注意
The private key must be kept secret at all times and must not be shared. Losing the private key means losing access to an account's funds, so make sure it is securely backed up.
It is advisable to turn central and cold wallets into multisig accounts to add two-factor authentication. The cosignatories of the multisig account become the account managers, so no transaction can be announced from the multisig account without the cosignatories' approval. Symbol's current implementation of multisig is “M-of-N” meaning that M out of the total N cosignatories of an account need to approve a transaction for it to be announced.
注意
Multisig accounts are a powerful yet dangerous tool. If access to some cosignatory account is lost and the minimum approval is not reached (the M above), access to the multisig account can be permanently lost. Always configure multisig accounts with caution.
To strengthen security, extra account restrictions can be added to the Exchange's accounts, like blocking announcing or receiving transactions given a series of rules.
The native currency of the Symbol network is named XYM
. The token is used to pay for transactions and service fees, which are used as well to provide an incentive for those participants who secure the network and run the infrastructure.
Tokens can be divided up to divisibility
decimal places. Amounts given without decimals are called absolute, whereas when decimals are used amounts are called relative. For example, when divisibility is 6, 1 relative token corresponds to 1'000'000 absolute tokens, and the smallest token is 0.000001 relative units. The smallest absolute unit is always 1, regardless of the divisibility.
これらは XYM
のプロパティです:
プロパティ |
値 |
説明 |
---|---|---|
ID |
|
トークン識別子 |
エイリアス |
|
トークンの親しみやすい名前 |
初期供給量 |
7'842'928'625 (relative) |
流通しているトークンユニットの初期供給量 |
最大供給量 |
8'999'999'999 (relative) |
Maximum amount of token units in circulation after inflation is applied |
Divisibility |
6 |
This means that the smallest fraction of the token is 0.000001 (relative). |
Duration |
0 |
トークンは失効しません |
Supply mutable |
False |
トークンの供給量は変更できません。 |
Transferable |
True |
トークンは任意のアカウント間で転送できます。 |
Restrictable |
False |
Token creator cannot restrict which accounts can transact with the mosaic |
注意
The XYM
token can be referred to through its native token ID or its friendlier alias symbol.xym
, which has an ID on itself.
On MAINNET, these IDs are 0x6BED913FA20223F8
(mosaic ID) and 0xE74B99BA41F4AFEE
(alias ID).
Always treat these two IDs as equivalent.
Symbol has a novel feature called アグリゲートトランザクション which allows bundling multiple inner transactions into a single one.
Therefore, when monitoring incoming Transfer transactions you must remember to also look inside all Aggregate transactions (Both Aggregate Complete and Aggregate Bonded transactions). The example code given below shows a way of achieving this.
注意
When Aggregate transactions are not monitored, inner transfer transactions are not detected, leading to lots of reports of lost funds.
This is specially relevant for multi-signature accounts, where all transactions are wrapped in an Aggregate.
This is a classic conflict in blockchain technology: On one hand, if transactions are accepted too quickly, they might need to be reverted later on in the event of a network fork. On the other hand, waiting for too long is inconvenient for users.
Symbol でこれを処理する方法は2つあります。
Symbol implements Finalization, a process that guarantees that blocks are immutable and therefore transactions are secure.
To know if a block has been finalized, check the latestFinalizedBlock
property in the /chain/info endpoint. All blocks with a height lower than (or equal to) latestFinalizedBlock.height
are finalized and are therefore immutable.
On average, blocks are finalized after 5 minutes, in the absence of network problems.
To have faster response times, one must ignore finalization and accept the risk that comes with this: Unfinalized blocks have a probability of being reverted, which decreases over time but is never zero until the block is finalized.
The procedure, which is common in blockchains which do not support finalization, is to wait for a few blocks to be validated (added to the blockchain) before accepting a transaction.
The amount of blocks to wait for depends on the risk one wants to accept. The recommendation for Symbol is 20 blocks (about 10 minutes, regardless of network conditions, because Finalization will almost always happen during this time).
In summary
Waiting for a fixed amount of blocks leads to consistent confirmation times, but has the risk that confirmed transactions might be reverted.
Waiting for finalization has variable confirmation times (5 minutes on average) but has zero rollback risk.
An added problem caused by rollbacks is that transactions might expire in the process of resolving a network fork.
A bit of context is required here. Transactions are not allowed to remain unconfirmed in the network forever, as this would pose a significant strain on the network's resources. Instead, all transactions have a deadline, and are automatically disposed of when the deadline arrives.
Users are free to use any deadline they want for their transactions, between now and 6h into the future (48h for アグリゲートボンド transactions).
Transactions which are about to expire are delicate because, even if they get confirmed and are added to the blockchain, a rollback could send them back to the unconfirmed state and their deadline could expire before they are confirmed again.
Therefore, it is recommended that:
Incoming transactions with a deadline less than 1h into the future are rejected with a warning message, for example:
Transaction is too close to expiration to be safely accepted.
Exchanges avoid using transactions with short lifespans.
Exchanges actively encourage their customers to avoid using transactions with short lifespans.
This guide shows snippets of code to exemplify the different processes. All snippets are based on the same program that can be found here. A few notes on this example program:
It uses a fake DBService
object that simulates the Exchange database. Calls to this object should obviously be replaced by the actual Exchange infrastructure in production code. For simplicity these calls are synchronous but they could be made asynchronously too.
No error handling is performed at all. Use mechanisms like try {} catch
where appropriate in production code.
Finally, besides the snippets shown in the guide, the complete program also contains auxiliary code (like polling loops) in order to make it runnable and self-sufficient. This auxiliary code is not meant to be used as an inspiration at all, it is just there for convenience.
Users perform deposits by announcing a regular transfer transaction using their wallet, moving the funds from their account directly to the Exchange Central Wallet. Since the transfer is handled entirely by the blockchain, the funds will be added to the Exchange Central Wallet without the Exchange's mediation, and this poses some problems:
The intended recipient of the transaction must be determined. This is done by attaching the user's UUID as the transaction's message.
The fact that a transaction has happened must be timely detected to update the user's account on the Exchange.
Transactions must be finalized to be 100% sure that they will not be rolled back.
次に提案されるコードは、ブロックチェーンの監視により、これらすべての問題に対処します。
The blockchain is polled periodically and all incoming transactions since last poll are processed in a batch:
All Transfer transactions added to the blockchain since the last check and up to the latest finalized block are examined, looking for the ones destined to the Central Exchange Wallet. This can be done efficiently with a single Symbol API call.
Transfer transactions embedded in Aggregate Complete and Aggregate Bonded transactions must also be examined (see the Aggregates section above). This is handled in the example code by the embedded: true
parameter in the searchConfirmedTransactions call.
If Finalization is not desired (see the ロールバックの回避 section above) you can search up to 20 blocks before the current chain height, for example.
次のトランザクションをフィルタリング:
メッセージがない、またはメッセージが存在する UUID に対応していない。
トークンを含んでいない、または symbol.xym
ではないトークン。
Have already been processed (as a security measure).
The remaining transactions are then processed:
データベース上のユーザアカウントにトークンは追加されました。
トランザクションは、そのハッシュをデータベースに追加することにより、処理済みとしてマークされます。
処理済みの最終ブロック高を保存し、次のポーリング期間まで待ちます。
The code snippet, using Symbol's TypeScript SDK is this:
// Mocks a database service
const dbService = new DBServiceImpl();
const config: ExchangeSymbolConfig = {
// Replace with your node URL
apiUrl: 'NODE_URL',
// Use MAIN_NET or TEST_NET
networkType: NetworkType.TEST_NET,
// Replace with value from http://<API-NODE-URL>:3000/network/properties
networkGenerationHashSeed:
'3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155',
// Replace with the account central address
centralAccountAddress: Address.createFromRawAddress(
'TAOXUJOTTW3W5XTBQMQEX3SQNA6MCUVGXLXR3TA',
),
// Use 6BED913FA20223F8 for MAIN_NET or 091F837E059AE13C for TEST_NET
tokenId: new MosaicId('091F837E059AE13C'),
tokenAlias: new NamespaceId('symbol.xym'),
tokenDivisibility: 6,
requiredConfirmations: 20,
useFinalization: true,
};
const repositoryFactory = new RepositoryFactoryHttp(config.apiUrl);
const chainHttp = repositoryFactory.createChainRepository();
const transactionHttp = repositoryFactory.createTransactionRepository();
const transactionPaginationStreamer = new TransactionPaginationStreamer(
transactionHttp,
);
// Get current chain height and latest finalized block
const {
height: currentHeight,
latestFinalizedBlock: finalizationBlock,
} = await chainHttp.getChainInfo().toPromise();
const maxHeight = config.useFinalization
? finalizationBlock.height
: currentHeight.subtract(UInt64.fromUint(config.requiredConfirmations));
// 1. Look for confirmed transactions destined to the central account address,
// in the desired block height range.
const searchCriteria = {
group: TransactionGroup.Confirmed,
recipientAddress: config.centralAccountAddress,
embedded: true,
order: Order.Asc,
type: [TransactionType.TRANSFER],
fromHeight: dbService.getLastProcessedHeight(),
toHeight: maxHeight,
} as TransactionSearchCriteria;
const data = await transactionPaginationStreamer
.search(searchCriteria)
.pipe(toArray() as any)
.toPromise();
// 2. Exclude invalid transactions
const results = (data as TransferTransaction[]).filter((transaction) => {
const transactionInfo = transaction.transactionInfo;
if (!transactionInfo) return false;
const transactionIndex = transactionInfo.index;
const transactionHash =
transactionInfo instanceof AggregateTransactionInfo
? transactionInfo.aggregateHash
: transactionInfo.hash ?? '';
return (
// 2.a
dbService.existsUser(transaction.message.payload) &&
// 2.b
transaction.mosaics.length === 1 &&
(transaction.mosaics[0].id.toHex() === config.tokenId.toHex() ||
transaction.mosaics[0].id.toHex() === config.tokenAlias.toHex()) &&
// 2.c
!dbService.existsTransaction(transactionHash, transactionIndex)
);
});
// 3. Record the valid deposits in the exchange database
results.forEach((transaction) => {
const transactionInfo = transaction.transactionInfo;
if (!transactionInfo) return;
const transactionHash =
transactionInfo instanceof AggregateTransactionInfo
? transactionInfo.aggregateHash
: transactionInfo.hash ?? '';
const transactionIndex = transactionInfo.index;
const amount =
transaction.mosaics[0].amount.compact() /
Math.pow(10, config.tokenDivisibility);
dbService.recordDeposit(
transaction.message.payload,
amount,
transactionHash,
transactionIndex,
);
});
// 4. Store the last height that has been processed
dbService.setLastProcessedHeight(maxHeight);
// Mocks a database service
const dbService = new DBServiceImpl_1.DBServiceImpl();
const config = {
// Replace with your node URL
apiUrl: 'NODE_URL',
// Use MAIN_NET or TEST_NET
networkType: symbol_sdk_1.NetworkType.TEST_NET,
// Replace with value from http://<API-NODE-URL>:3000/network/properties
networkGenerationHashSeed:
'3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155',
// Replace with the account central address
centralAccountAddress: symbol_sdk_1.Address.createFromRawAddress(
'TAOXUJOTTW3W5XTBQMQEX3SQNA6MCUVGXLXR3TA',
),
// Use 6BED913FA20223F8 for MAIN_NET or 091F837E059AE13C for TEST_NET
tokenId: new symbol_sdk_1.MosaicId('091F837E059AE13C'),
tokenAlias: new symbol_sdk_1.NamespaceId('symbol.xym'),
tokenDivisibility: 6,
requiredConfirmations: 20,
useFinalization: true,
};
const repositoryFactory = new symbol_sdk_1.RepositoryFactoryHttp(
config.apiUrl,
);
const chainHttp = repositoryFactory.createChainRepository();
const transactionHttp = repositoryFactory.createTransactionRepository();
const transactionPaginationStreamer = new symbol_sdk_1.TransactionPaginationStreamer(
transactionHttp,
);
// Get current chain height and latest finalized block
const {
height: currentHeight,
latestFinalizedBlock: finalizationBlock,
} = await chainHttp.getChainInfo().toPromise();
const maxHeight = config.useFinalization
? finalizationBlock.height
: currentHeight.subtract(
symbol_sdk_1.UInt64.fromUint(config.requiredConfirmations),
);
// 1. Look for confirmed transactions destined to the central account address,
// in the desired block height range.
const searchCriteria = {
group: symbol_sdk_1.TransactionGroup.Confirmed,
recipientAddress: config.centralAccountAddress,
embedded: true,
order: symbol_sdk_1.Order.Asc,
type: [symbol_sdk_1.TransactionType.TRANSFER],
fromHeight: dbService.getLastProcessedHeight(),
toHeight: maxHeight,
};
const data = await transactionPaginationStreamer
.search(searchCriteria)
.pipe(operators_1.toArray())
.toPromise();
// 2. Exclude invalid transactions
const results = data.filter((transaction) => {
var _a;
const transactionInfo = transaction.transactionInfo;
if (!transactionInfo) return false;
const transactionIndex = transactionInfo.index;
const transactionHash =
transactionInfo instanceof symbol_sdk_1.AggregateTransactionInfo
? transactionInfo.aggregateHash
: (_a = transactionInfo.hash) !== null && _a !== void 0
? _a
: '';
return (
// 2.a
dbService.existsUser(transaction.message.payload) &&
// 2.b
transaction.mosaics.length === 1 &&
(transaction.mosaics[0].id.toHex() === config.tokenId.toHex() ||
transaction.mosaics[0].id.toHex() === config.tokenAlias.toHex()) &&
// 2.c
!dbService.existsTransaction(transactionHash, transactionIndex)
);
});
// 3. Record the valid deposits in the exchange database
results.forEach((transaction) => {
var _a;
const transactionInfo = transaction.transactionInfo;
if (!transactionInfo) return;
const transactionHash =
transactionInfo instanceof symbol_sdk_1.AggregateTransactionInfo
? transactionInfo.aggregateHash
: (_a = transactionInfo.hash) !== null && _a !== void 0
? _a
: '';
const transactionIndex = transactionInfo.index;
const amount =
transaction.mosaics[0].amount.compact() /
Math.pow(10, config.tokenDivisibility);
dbService.recordDeposit(
transaction.message.payload,
amount,
transactionHash,
transactionIndex,
);
});
// 4. Store the last height that has been processed
dbService.setLastProcessedHeight(maxHeight);
All configuration data is held in the ExchangeSymbolConfig
object including the selection of the finalization mechanism.
The above code snippet should be called in a loop every minute, for example, and it will process all new valid transactions that have already been finalized (or that have waited enough blocks, depending on the chosen method).
However, transactions will not be reported immediately, and this might be annoying for users. Using WebSockets transactions can be monitored in real-time and a notification can be shown to the user as soon as a transaction is confirmed on the network (or even as soon as it is announced on the network).
These transactions, though, should be clearly marked as pending and not acted upon until verified by the above code, to avoid rollbacks.
Related links
Users send withdrawal requests to the Exchange Server, via a web page or mobile app, for example. If the database indicates that the user has enough funds to perform the withdrawal, a transfer transaction is announced from the Exchange Central Wallet to the Symbol address indicated in the request.
Announcing the transaction has a fee, which is paid by the Exchange Central Wallet but can be deduced from the user's account. Regardless of the token being transferred, fees are always paid in XYM tokens.
The withdrawal process requires two steps: First the transaction transferring the funds is announced and confirmed (added to the blockchain). Afterwards, the exchange needs to wait for the transaction to be finalized, as explained in the ロールバックの回避 section above.
The withdrawal transaction is just a regular Symbol transfer transaction. The code looks long because it contains a lot of repeated boilerplate, to make it self-contained:
設定は ExchangeSymbolConfig
オブジェクトに保存されます。
多くのリポジトリは RepositoryFactoryHttp
クラスを介してインスタンス化されます。
The withdrawal details are retrieved from environment variables in this example.
Then:
実際のトランザクションは TransferTransaction.create
を使用して作成します。
トランザクションは署名済みです。
The signed transaction is announced using the TransactionService
to simplify waiting for its confirmation.
// Mocks a database service
const dbService = new DBServiceImpl();
// Exchange configuration
const config: ExchangeSymbolConfig = {
// Replace with your node URL
apiUrl: 'NODE_URL',
// Use MAIN_NET or TEST_NET
networkType: NetworkType.TEST_NET,
// Replace with value from http://<API-NODE-URL>:3000/network/properties
networkGenerationHashSeed:
'3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155',
// Replace with the account central address
centralAccountAddress: Address.createFromRawAddress(
'TAOXUJOTTW3W5XTBQMQEX3SQNA6MCUVGXLXR3TA',
),
// Use 6BED913FA20223F8 for MAIN_NET or 091F837E059AE13C for TEST_NET
tokenId: new MosaicId('091F837E059AE13C'),
tokenAlias: new NamespaceId('symbol.xym'),
tokenDivisibility: 6,
requiredConfirmations: 20,
useFinalization: true,
};
// Repositories
const repositoryFactory = new RepositoryFactoryHttp(config.apiUrl);
const listener = repositoryFactory.createListener();
const transactionHttp = repositoryFactory.createTransactionRepository();
const chainHttp = repositoryFactory.createChainRepository();
const transactionService = new TransactionService(
transactionHttp,
repositoryFactory.createReceiptRepository(),
);
const transactionPaginationStreamer = new TransactionPaginationStreamer(
transactionHttp,
);
// Replace with exchange account private key
const exchangeAccountPrivateKey = process.env.PRIVATE_KEY as string;
const exchangeAccount = Account.createFromPrivateKey(
exchangeAccountPrivateKey,
config.networkType,
);
// Replace with destination address from user's request
const userRawAddress = process.env.RECIPIENT_ADDRESS as string;
const userAddress = Address.createFromRawAddress(userRawAddress);
// Replace with the source UUID from user's request
const uuid = process.env.UUID as string;
// Replace with amount of tokens to transfer from user's request
const relativeAmount = parseFloat(process.env.AMOUNT as string);
// Check that the user has enough funds
if (dbService.getUserAmount(uuid, config.tokenId) < relativeAmount) {
throw Error('User ' + uuid + ' does not have enough funds.');
}
// 1. Create withdrawal transaction
const absoluteAmount =
relativeAmount * Math.pow(10, config.tokenDivisibility);
const token = new Mosaic(config.tokenId, UInt64.fromUint(absoluteAmount));
const epochAdjustment = await repositoryFactory
.getEpochAdjustment()
.toPromise();
const withdrawalTransaction = TransferTransaction.create(
Deadline.create(epochAdjustment),
userAddress,
[token],
EmptyMessage,
config.networkType,
UInt64.fromUint(200000), // Fixed max fee of 0.2 XYM
);
// 2. Sign transaction
const signedTransaction = exchangeAccount.sign(
withdrawalTransaction,
config.networkGenerationHashSeed,
);
// 3. Announce transaction and wait for confirmation
console.log('Announcing transaction', signedTransaction.hash);
await listener.open();
const transaction = await transactionService
.announce(signedTransaction, listener)
.toPromise();
console.log(
'Transaction confirmed at height',
transaction.transactionInfo?.height.compact() ?? 0,
);
listener.close();
// Mocks a database service
const dbService = new DBServiceImpl_1.DBServiceImpl();
// Exchange configuration
const config = {
// Replace with your node URL
apiUrl: 'NODE_URL',
// Use MAIN_NET or TEST_NET
networkType: symbol_sdk_1.NetworkType.TEST_NET,
// Replace with value from http://<API-NODE-URL>:3000/network/properties
networkGenerationHashSeed:
'3B5E1FA6445653C971A50687E75E6D09FB30481055E3990C84B25E9222DC1155',
// Replace with the account central address
centralAccountAddress: symbol_sdk_1.Address.createFromRawAddress(
'TAOXUJOTTW3W5XTBQMQEX3SQNA6MCUVGXLXR3TA',
),
// Use 6BED913FA20223F8 for MAIN_NET or 091F837E059AE13C for TEST_NET
tokenId: new symbol_sdk_1.MosaicId('091F837E059AE13C'),
tokenAlias: new symbol_sdk_1.NamespaceId('symbol.xym'),
tokenDivisibility: 6,
requiredConfirmations: 20,
useFinalization: true,
};
// Repositories
const repositoryFactory = new symbol_sdk_1.RepositoryFactoryHttp(
config.apiUrl,
);
const listener = repositoryFactory.createListener();
const transactionHttp = repositoryFactory.createTransactionRepository();
const chainHttp = repositoryFactory.createChainRepository();
const transactionService = new symbol_sdk_1.TransactionService(
transactionHttp,
repositoryFactory.createReceiptRepository(),
);
const transactionPaginationStreamer = new symbol_sdk_1.TransactionPaginationStreamer(
transactionHttp,
);
// Replace with exchange account private key
const exchangeAccountPrivateKey = process.env.PRIVATE_KEY;
const exchangeAccount = symbol_sdk_1.Account.createFromPrivateKey(
exchangeAccountPrivateKey,
config.networkType,
);
// Replace with destination address from user's request
const userRawAddress = process.env.RECIPIENT_ADDRESS;
const userAddress = symbol_sdk_1.Address.createFromRawAddress(userRawAddress);
// Replace with the source UUID from user's request
const uuid = process.env.UUID;
// Replace with amount of tokens to transfer from user's request
const relativeAmount = parseFloat(process.env.AMOUNT);
// Check that the user has enough funds
if (dbService.getUserAmount(uuid, config.tokenId) < relativeAmount) {
throw Error('User ' + uuid + ' does not have enough funds.');
}
// 1. Create withdrawal transaction
const absoluteAmount =
relativeAmount * Math.pow(10, config.tokenDivisibility);
const token = new symbol_sdk_1.Mosaic(
config.tokenId,
symbol_sdk_1.UInt64.fromUint(absoluteAmount),
);
const epochAdjustment = await repositoryFactory
.getEpochAdjustment()
.toPromise();
const withdrawalTransaction = symbol_sdk_1.TransferTransaction.create(
symbol_sdk_1.Deadline.create(epochAdjustment),
userAddress,
[token],
symbol_sdk_1.EmptyMessage,
config.networkType,
symbol_sdk_1.UInt64.fromUint(200000),
);
// 2. Sign transaction
const signedTransaction = exchangeAccount.sign(
withdrawalTransaction,
config.networkGenerationHashSeed,
);
// 3. Announce transaction and wait for confirmation
console.log('Announcing transaction', signedTransaction.hash);
await listener.open();
const transaction = await transactionService
.announce(signedTransaction, listener)
.toPromise();
console.log(
'Transaction confirmed at height',
(_b =
(_a = transaction.transactionInfo) === null || _a === void 0
? void 0
: _a.height.compact()) !== null && _b !== void 0
? _b
: 0,
);
listener.close();
Multi-signature accounts
When the Exchange Central Wallet is a multi-signature account announcing the transaction is slightly more complex, as it involves the central wallet and its cosignatories. See the following resources:
Once the transaction is confirmed, the next step is to wait for it to be finalized to make sure it cannot be reverted. Until then, it should be marked as pending and not acted upon.
Waiting for finalization is performed in a manner very similar to how incoming deposits are monitored (see 預け入れ above): The blockchain is polled periodically and all transactions since the last check are processed in a batch, looking for outgoing transfers which have already been finalized.
The following code snippet should be run in a loop every minute, for example, and it will search for finalized withdrawal operations from the Exchange and record them in the Exchange's database.
このスニペットは 上記で述べた 同じループで実行します。
// Get current chain height and latest finalized block
const {
height: currentHeight,
latestFinalizedBlock: finalizationBlock,
} = await chainHttp.getChainInfo().toPromise();
const maxHeight = config.useFinalization
? finalizationBlock.height
: currentHeight.subtract(UInt64.fromUint(config.requiredConfirmations));
// Bail out if there have been no new (final) transactions since last check
const lastProcessedHeight = dbService.getLastProcessedHeight();
if (lastProcessedHeight.equals(maxHeight)) return;
// Look for confirmed transactions signed by the central account address,
// in the desired block height range.
const searchCriteria = {
group: TransactionGroup.Confirmed,
signerPublicKey: exchangeAccount.publicKey,
embedded: true,
order: Order.Asc,
type: [TransactionType.TRANSFER],
fromHeight: lastProcessedHeight.add(UInt64.fromUint(1)),
toHeight: maxHeight,
} as TransactionSearchCriteria;
const data = await transactionPaginationStreamer
.search(searchCriteria)
.pipe(toArray() as any)
.toPromise();
console.log(
'Processing',
(data as TransferTransaction[]).length,
'entries from height',
searchCriteria.fromHeight?.compact(),
'to',
searchCriteria.toHeight?.compact(),
);
// Record the valid withdrawals in the exchange database
(data as TransferTransaction[]).forEach((transaction) => {
const transactionInfo = transaction.transactionInfo;
if (!transactionInfo) return;
const transactionHash =
transactionInfo instanceof AggregateTransactionInfo
? transactionInfo.aggregateHash
: transactionInfo.hash ?? '';
const amount =
transaction.mosaics[0].amount.compact() /
Math.pow(10, config.tokenDivisibility);
dbService.recordWithdrawal(uuid, amount, transactionHash);
});
// Store the last height that has been processed
dbService.setLastProcessedHeight(maxHeight);
// Get current chain height and latest finalized block
const {
height: currentHeight,
latestFinalizedBlock: finalizationBlock,
} = await chainHttp.getChainInfo().toPromise();
const maxHeight = config.useFinalization
? finalizationBlock.height
: currentHeight.subtract(
symbol_sdk_1.UInt64.fromUint(config.requiredConfirmations),
);
// Bail out if there have been no new (final) transactions since last check
const lastProcessedHeight = dbService.getLastProcessedHeight();
if (lastProcessedHeight.equals(maxHeight)) return;
// Look for confirmed transactions signed by the central account address,
// in the desired block height range.
const searchCriteria = {
group: symbol_sdk_1.TransactionGroup.Confirmed,
signerPublicKey: exchangeAccount.publicKey,
embedded: true,
order: symbol_sdk_1.Order.Asc,
type: [symbol_sdk_1.TransactionType.TRANSFER],
fromHeight: lastProcessedHeight.add(symbol_sdk_1.UInt64.fromUint(1)),
toHeight: maxHeight,
};
const data = await transactionPaginationStreamer
.search(searchCriteria)
.pipe(operators_1.toArray())
.toPromise();
console.log(
'Processing',
data.length,
'entries from height',
(_a = searchCriteria.fromHeight) === null || _a === void 0
? void 0
: _a.compact(),
'to',
(_b = searchCriteria.toHeight) === null || _b === void 0
? void 0
: _b.compact(),
);
// Record the valid withdrawals in the exchange database
data.forEach((transaction) => {
var _a;
const transactionInfo = transaction.transactionInfo;
if (!transactionInfo) return;
const transactionHash =
transactionInfo instanceof symbol_sdk_1.AggregateTransactionInfo
? transactionInfo.aggregateHash
: (_a = transactionInfo.hash) !== null && _a !== void 0
? _a
: '';
const amount =
transaction.mosaics[0].amount.compact() /
Math.pow(10, config.tokenDivisibility);
dbService.recordWithdrawal(uuid, amount, transactionHash);
});
// Store the last height that has been processed
dbService.setLastProcessedHeight(maxHeight);
Related links