트랜잭션 빌드 가이드
서명 대상이 되는 원본 트랜잭션(serializedUnsignedTransaction)을 생성하는 방법을 설명합니다.
여기서 생성한 값은 옥텟 API의 출금 신청 또는 트랜잭션 서명 신청 API의 입력값으로 사용됩니다.
비트코인
비트코인은 UTXO 기반이므로 보내는 주소가 보유한 UTXO 목록이 필요합니다.
UTXO는 옥텟 API의 UTXO 목록 조회 API로 조회할 수 있습니다.
utxos: 보내는 주소가 보유한 UTXO 목록. UTXO 목록 조회 API 응답의txid,outputIndex(→vout),script를 그대로 사용하고,amount는 BTC 단위이므로 satoshi로 변환하여value에 넣습니다.feeRate: 수수료율 (sat/vByte). 옥텟 API의 지갑에 등록된 자산의 수수료 조회 API로 조회할 수 있으며, 응답의withdrawalFee.gasPrice값이 비트코인의 수수료율(sat/vByte)에 해당합니다.coinselect가 수수료를 반영해 사용할 UTXO를 선택하고 잔돈(change) output을 계산합니다.- 옥텟의 비트코인 기본 주소 타입은 P2WPKH(SegWit,
bc1으로 시작)이므로 예시도 P2WPKH 기준이며,witnessUtxo로 input을 구성합니다. 빌드 결과물은 PSBT hex입니다.
// bitcoinjs-lib: 6.1.7
// coinselect: 3.1.13
import * as bitcoin from 'bitcoinjs-lib';
const coinSelect = require('coinselect');
/*
NETWORK
mainnet: bitcoin.networks.bitcoin
testnet: bitcoin.networks.testnet
*/
const network = bitcoin.networks.bitcoin;
const fromAddress = 'bc1qm3p027vycz73t99ewclkjz53c4qkyswmcnwve9'; // 보내는 주소 (잔돈 수령 주소)
const toAddress = 'bc1ql6tme8r3wk88nuldy2eguaz7rgtg0efhl7h9v7'; // 받는 주소
const amount = 6000000; // 보낼 금액 (satoshi)
const feeRate = 10; // 수수료율 (sat/vByte, 수수료 조회 API 응답의 withdrawalFee.gasPrice)
// 보내는 주소가 보유한 UTXO 목록 (UTXO 목록 조회 API로 조회)
const utxos = [
{
txid: 'ff219ded839a14010222985c4f314f123a50b5d6984c8c8d41f1ad037ef8e9a9', // UTXO 트랜잭션 해시
vout: 1, // UTXO output 인덱스 (조회 응답의 outputIndex)
value: 5000000, // UTXO 금액 (satoshi)
script: '0014dc42f57984c0bd1594b9763f690a91c5416241db', // UTXO의 scriptPubKey (hex)
},
{
txid: 'b4f5e8a209cc41d6d24f7f8ccde3f7a5be9d9ad3ad3f7f6a30cf50f4406eab12',
vout: 0,
value: 3000000,
script: '0014dc42f57984c0bd1594b9763f690a91c5416241db',
},
];
// 수수료를 반영해 사용할 input과 output(잔돈 포함)을 계산
const { inputs, outputs } = coinSelect(utxos, [{ address: toAddress, value: amount }], feeRate);
if (!inputs || !outputs) {
throw new Error('Insufficient funds');
}
const psbt = new bitcoin.Psbt({ network });
for (const input of inputs) {
psbt.addInput({
hash: Buffer.from(input.txid, 'hex').reverse(), // txid는 little-endian으로 변환
index: input.vout,
witnessUtxo: {
script: Buffer.from(input.script, 'hex'),
value: input.value,
},
});
}
for (const output of outputs) {
// address가 없는 output은 잔돈(change)이므로 보내는 주소로 되돌려 받는다
psbt.addOutput({
address: output.address ?? fromAddress,
value: output.value,
});
}
const serializedUnsignedTransaction = psbt.toHex();
// 70736274ff01009a0200000002a9e9f87e03adf1418d8c4c98d6b5503a124f314f5c98220201149a83ed9d21ff0100000000ffffffff12ab6e40f450cf306a7f3fadd39a9dbea5f7e3cd8c7f4fd2d641cc09a2e8f5b40000000000ffffffff02808d5b0000000000160014fe97bc9c71758e79f3ed22b28e745e1a1687e537d07a1e0000000000160014dc42f57984c0bd1594b9763f690a91c5416241db000000000001011f404b4c0000000000160014dc42f57984c0bd1594b9763f690a91c5416241db0001011fc0c62d0000000000160014dc42f57984c0bd1594b9763f690a91c5416241db000000이더리움
nonce: 보내는 주소의 트랜잭션 카운트 (eth_getTransactionCount로 조회)maxFeePerGas/maxPriorityFeePerGas: EIP-1559 가스 가격 (gwei)- 네이티브 코인 전송의
gasLimit은 21000 고정, ERC-20 전송은 토큰별로 다르므로 여유 있게 설정하거나eth_estimateGas로 추정합니다.
네이티브 코인 전송
// @ethereumjs/tx: 5.4.0
// @ethereumjs/common: 4.4.0
// web3: 4.16.0
import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx';
import { Common } from '@ethereumjs/common';
import { Web3 } from 'web3';
const web3 = new Web3();
/*
CHAIN_ID
mainnet: 1
testnet(sepolia): 11155111
*/
const CHAIN_ID = 1;
const HARDFORK = 'london';
const toAddress = '0x4c81fec94ab7a764667ad35b55190cbeb4cd4a34'; // 받는 주소
const amount = '0.02'; // 보낼 금액 (ETH)
const nonce = 0; // 보내는 주소의 트랜잭션 카운트 (eth_getTransactionCount)
const maxFeePerGasGwei = '41'; // 최대 가스 가격 (gwei)
const maxPriorityFeePerGasGwei = '1'; // 우선순위 수수료 (gwei)
const txData: any = {
type: '0x02',
nonce: web3.utils.toHex(nonce),
to: toAddress,
value: web3.utils.toHex(BigInt(web3.utils.toWei(amount, 'ether'))),
gasLimit: web3.utils.toHex(21000),
maxFeePerGas: web3.utils.toHex(BigInt(web3.utils.toWei(maxFeePerGasGwei, 'gwei'))),
maxPriorityFeePerGas: web3.utils.toHex(BigInt(web3.utils.toWei(maxPriorityFeePerGasGwei, 'gwei'))),
};
const common = Common.custom({ chainId: CHAIN_ID }, { hardfork: HARDFORK });
const tx = FeeMarketEIP1559Transaction.fromTxData(txData, { common });
const serializedUnsignedTransaction = `0x${Buffer.from(tx.serialize()).toString('hex')}`;
// 0x02f20180843b9aca0085098bca5a00825208944c81fec94ab7a764667ad35b55190cbeb4cd4a3487470de4df82000080c0808080위 출력값은 트랜잭션 서명 가이드의 이더리움 예시 입력값과 동일합니다.
ERC-20 토큰 전송
컨트랙트의 transfer(to, value) 호출을 ABI 인코딩하여 data에 넣고, to는 토큰 컨트랙트 주소로 설정합니다.
value(전송 금액)는 토큰의 decimals가 적용된 최소 단위 정수로 전달합니다.
// @ethereumjs/tx: 5.4.0
// @ethereumjs/common: 4.4.0
// web3: 4.16.0
import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx';
import { Common } from '@ethereumjs/common';
import { Web3, Contract } from 'web3';
const web3 = new Web3();
/*
CHAIN_ID
mainnet: 1
testnet(sepolia): 11155111
*/
const CHAIN_ID = 1;
const HARDFORK = 'london';
const Erc20TransferAbi: any = [
{
inputs: [
{ name: '_to', type: 'address' },
{ name: '_value', type: 'uint256' },
],
name: 'transfer',
outputs: [{ name: '', type: 'bool' }],
stateMutability: 'nonpayable',
type: 'function',
},
];
const contractAddress = '0xdac17f958d2ee523a2206206994597c13d831ec7'; // 토큰 컨트랙트 주소 (예시: USDT)
const toAddress = '0x4c81fec94ab7a764667ad35b55190cbeb4cd4a34'; // 받는 주소
const amount = '12500000'; // 보낼 금액 - 토큰 decimals가 적용된 최소 단위 (12.5 USDT, decimals: 6)
const nonce = 0; // 보내는 주소의 트랜잭션 카운트 (eth_getTransactionCount)
const maxFeePerGasGwei = '41'; // 최대 가스 가격 (gwei)
const maxPriorityFeePerGasGwei = '1'; // 우선순위 수수료 (gwei)
const contract = new Contract(Erc20TransferAbi, contractAddress);
const data = contract.methods.transfer(toAddress, amount).encodeABI();
const txData: any = {
type: '0x02',
nonce: web3.utils.toHex(nonce),
to: contractAddress,
value: '0x0',
data,
gasLimit: web3.utils.toHex(100000),
maxFeePerGas: web3.utils.toHex(BigInt(web3.utils.toWei(maxFeePerGasGwei, 'gwei'))),
maxPriorityFeePerGas: web3.utils.toHex(BigInt(web3.utils.toWei(maxPriorityFeePerGasGwei, 'gwei'))),
};
const common = Common.custom({ chainId: CHAIN_ID }, { hardfork: HARDFORK });
const tx = FeeMarketEIP1559Transaction.fromTxData(txData, { common });
const serializedUnsignedTransaction = `0x${Buffer.from(tx.serialize()).toString('hex')}`;
// 0x02f8710180843b9aca0085098bca5a00830186a094dac17f958d2ee523a2206206994597c13d831ec780b844a9059cbb0000000000000000000000004c81fec94ab7a764667ad35b55190cbeb4cd4a340000000000000000000000000000000000000000000000000000000000bebc20c0808080바이낸스스마트체인
바이낸스스마트체인은 Legacy 트랜잭션(gasPrice)을 사용합니다.
BEP-20 토큰 전송은 이더리움의 ERC-20 예시와 동일한 방식으로 data를 인코딩하고, 아래와 같이 LegacyTransaction + gasPrice로 빌드하면 됩니다.
// @ethereumjs/tx: 5.4.0
// @ethereumjs/common: 4.4.0
// web3: 4.16.0
import { LegacyTransaction } from '@ethereumjs/tx';
import { Common } from '@ethereumjs/common';
import { Web3 } from 'web3';
const web3 = new Web3();
/*
CHAIN_ID
mainnet: 56
testnet: 97
*/
const CHAIN_ID = 56;
const toAddress = '0x4c81fec94ab7a764667ad35b55190cbeb4cd4a34'; // 받는 주소
const amount = '0.02'; // 보낼 금액 (BNB)
const nonce = 0; // 보내는 주소의 트랜잭션 카운트 (eth_getTransactionCount)
const gasPriceGwei = '41'; // 가스 가격 (gwei)
const txData: any = {
nonce: web3.utils.toHex(nonce),
to: toAddress,
value: web3.utils.toHex(BigInt(web3.utils.toWei(amount, 'ether'))),
gasLimit: web3.utils.toHex(21000),
gasPrice: web3.utils.toHex(BigInt(web3.utils.toWei(gasPriceGwei, 'gwei'))),
};
const common = Common.custom({ chainId: CHAIN_ID });
const tx = LegacyTransaction.fromTxData(txData, { common });
const serializedUnsignedTransaction = `0x${Buffer.from(tx.serialize()).toString('hex')}`;
// 0xeb8085098bca5a00825208944c81fec94ab7a764667ad35b55190cbeb4cd4a3487470de4df82000080808080위 출력값은 트랜잭션 서명 가이드의 바이낸스스마트체인 예시 입력값과 동일합니다.
Updated about 19 hours ago
Did this page help you?
