コンテンツにスキップ
上級

Symbolとイーサリアム間のクロスチェーンスワップ⚓︎

2つの当事者であるアリスとボブは、互いを信頼したり仲介者を使用したりすることなく、0.01 ETH(イーサリアム上)と1 XYM(Symbol上)を交換したいと考えています。

CrossChainOverviewclusterSymbolSymbolclusterEthereumイーサリアムAliceSアリスBobSボブAliceS->BobS1 XYMAliceEアリスBobEボブAliceE->BobE0.01 ETH

トークンは2つの異なるブロックチェーン上に存在するため、直接の転送は不可能です。 両方のトークンがSymbol上にある場合、この交換は アトミックスワップ のチュートリアルで示されているように、単一の アグリゲートトランザクション で行えます。 トークンは異なるチェーン上に存在するため、代わりに クロスチェーンスワップ を使用してスワップを調整する必要があります。

このチュートリアルでは、イーサリアム上の HTLC スマートコントラクトとSymbolのネイティブトランザクションを使用して、チェーン間でこのトークンスワップを実行する方法を示します。

両方のチェーンとやり取りするために、このチュートリアルではSymbol SDKとイーサリアムクライアントライブラリを使用します。

サポートされているチェーン

このチュートリアルではSymbolとイーサリアム間のスワップを実演していますが、Symbolのシークレットロックの仕組みは、HTLCをサポートする任意のブロックチェーンで機能します。

HTLCプロトコル、タイミングの制約、および制限の背景については、クロスチェーンスワップ の概念ページを参照してください。

前提条件⚓︎

始める前に、以下のことを確認してください。

  • 開発環境をセットアップする。 開発環境のセットアップを参照してください。
  • アリス用とボブ用に、2つのSymbol アカウント を作成する。 秘密鍵からのアカウントの作成を参照してください。
  • ボブのアカウントで、シークレットロックのトランザクション手数料とロックされる金額を支払うためのXYMを取得する。 フォーセットからテストネット資金を取得するを参照してください。
  • アリス用とボブ用に、2つのイーサリアムアカウントを作成する。 Foundrycast wallet new コマンド、またはMetaMaskなどの任意のイーサリアムウォレットを使用できます。
  • ガス代を支払うために両方のイーサリアムアカウントにSepoliaテストネットETHを用意し、HTLCに資金を供給するのに十分な額をアリスのアカウントに用意する。 Sepolia ETHは、Google Cloud faucet またはその他のイーサリアムテストネットフォーセットから取得できます。

  • 使用する言語のイーサリアムライブラリをインストールする。

    pip install web3
    
    npm install ethers
    

完全なコード⚓︎

このチュートリアルの完全なコード一覧を以下に示します。 詳細な手順ごとの説明は次のセクションで行います。

import hashlib
import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import Hash256, PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.symbol.IdGenerator import generate_mosaic_alias_id
from symbolchain.symbol.Network import NetworkTimestamp
from symbolchain.sc import Amount
from web3 import Web3

SYMBOL_NODE_URL = os.getenv(
    'SYMBOL_NODE_URL', 'https://reference.symboltest.net:3001')
print(f'Using Symbol node {SYMBOL_NODE_URL}')

ETH_RPC_URL = os.getenv('ETH_RPC_URL',
    'https://ethereum-sepolia-rpc.publicnode.com')
print(f'Using Ethereum RPC {ETH_RPC_URL}')

# Ethereum HTLC contract on Sepolia
HTLC_ADDRESS = '0xd58e030bd21c7788897aE5Ea845DaBA936e91D2B'
HTLC_ABI = [
    {
        'name': 'newContract',
        'type': 'function',
        'stateMutability': 'payable',
        'inputs': [
            {'name': '_receiver', 'type': 'address'},
            {'name': '_hashlock', 'type': 'bytes32'},
            {'name': '_timelock', 'type': 'uint256'}
        ],
        'outputs': [{'name': 'contractId', 'type': 'bytes32'}]
    },
    {
        'name': 'withdraw',
        'type': 'function',
        'stateMutability': 'nonpayable',
        'inputs': [
            {'name': '_contractId', 'type': 'bytes32'},
            {'name': '_preimage', 'type': 'bytes'}
        ],
        'outputs': [{'name': '', 'type': 'bool'}]
    },
    {
        'name': 'getContract',
        'type': 'function',
        'stateMutability': 'view',
        'inputs': [
            {'name': '_contractId', 'type': 'bytes32'}
        ],
        'outputs': [
            {'name': 'sender', 'type': 'address'},
            {'name': 'receiver', 'type': 'address'},
            {'name': 'amount', 'type': 'uint256'},
            {'name': 'hashlock', 'type': 'bytes32'},
            {'name': 'timelock', 'type': 'uint256'},
            {'name': 'withdrawn', 'type': 'bool'},
            {'name': 'refunded', 'type': 'bool'},
            {'name': 'preimage', 'type': 'bytes'}
        ]
    },
    {
        'name': 'LogHTLCNew',
        'type': 'event',
        'inputs': [
            {'name': 'contractId', 'type': 'bytes32', 'indexed': True},
            {'name': 'sender', 'type': 'address', 'indexed': True},
            {'name': 'receiver', 'type': 'address', 'indexed': True},
            {'name': 'amount', 'type': 'uint256', 'indexed': False},
            {'name': 'hashlock', 'type': 'bytes32', 'indexed': False},
            {'name': 'timelock', 'type': 'uint256', 'indexed': False}
        ]
    }
]

# Helper function to fetch current Symbol network time
def get_network_time():
    time_path = '/node/time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(
        f'{SYMBOL_NODE_URL}{time_path}'
    ) as response:
        response_json = json.loads(response.read().decode())
        timestamp = NetworkTimestamp(int(
            response_json['communicationTimestamps'][
                'receiveTimestamp']))
        print(f'  Network time: {timestamp.timestamp}'
            ' ms since nemesis')
        return timestamp


# Helper function to fetch recommended Symbol fee multiplier
def get_fee_multiplier():
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(
        f'{SYMBOL_NODE_URL}{fee_path}'
    ) as response:
        response_json = json.loads(response.read().decode())
        median_mult = response_json['medianFeeMultiplier']
        minimum_mult = response_json['minFeeMultiplier']
        fee_mult = max(median_mult, minimum_mult)
        print(f'  Fee multiplier: {fee_mult}')
        return fee_mult


# Helper function to announce a Symbol transaction
def announce_transaction(payload, endpoint, label):
    print(f'Announcing {label} to {endpoint}')
    request = urllib.request.Request(
        f'{SYMBOL_NODE_URL}{endpoint}',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='PUT'
    )
    with urllib.request.urlopen(request) as response:
        print(f'  Response: {response.read().decode()}')


# Helper function to wait for Symbol transaction status
def wait_for_status(hash_value, expected_status, label):
    print(f'Waiting for {label} to reach {expected_status} status...')
    attempts = 0
    max_attempts = 60

    while attempts < max_attempts:
        try:
            url = (f'{SYMBOL_NODE_URL}/transactionStatus/{hash_value}')
            with urllib.request.urlopen(url) as response:
                status = json.loads(response.read().decode())
                print(f'  Transaction status: {status["group"]}')

                if status['group'] == 'failed':
                    raise Exception(f'{label} failed: {status["code"]}')

                if status['group'] == expected_status:
                    print(f'{label} {expected_status}'
                        f' in {attempts} seconds')
                    return

        except urllib.error.HTTPError as e:
            if e.code != 404:
                raise
            print('  Transaction status: not yet available')

        attempts += 1
        time.sleep(1)

    raise Exception(
        f'{label} not {expected_status} after {max_attempts} attempts')


# Poll Symbol for a confirmed secret proof transaction matching
# a hashlock.
def wait_for_secret_proof(signer_address, hashlock):
    hashlock_hex = hashlock.hex().upper()
    url = (f'{SYMBOL_NODE_URL}/transactions/confirmed'
        f'?address={signer_address}&type=16978&order=desc')
    print(f'Polling {url}')
    print(f'  Looking for secret: {hashlock_hex}')

    attempts = 0
    max_attempts = 60
    while attempts < max_attempts:
        with urllib.request.urlopen(url) as response:
            data = json.loads(response.read().decode())
        for tx in data.get('data', []):
            secret = tx['transaction'].get('secret', '')
            if secret.upper() == hashlock_hex:
                print(f'  Found proof transaction after {attempts}s')
                return bytes.fromhex(tx['transaction']['proof'])
        attempts += 1
        time.sleep(1)

    raise Exception(
        f'Secret proof not found after {max_attempts} attempts')


# Symbol accounts
facade = SymbolFacade('testnet')

ALICE_XYM_PRIVATE_KEY = os.getenv('ALICE_XYM_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
alice_xym_key_pair = SymbolFacade.KeyPair(
    PrivateKey(ALICE_XYM_PRIVATE_KEY))
alice_xym_address = facade.network.public_key_to_address(
    alice_xym_key_pair.public_key)
print(f'Alice Symbol address: {alice_xym_address}')

BOB_XYM_PRIVATE_KEY = os.getenv('BOB_XYM_PRIVATE_KEY',
    '1111111111111111111111111111111111111111111111111111111111111111')
bob_xym_key_pair = SymbolFacade.KeyPair(PrivateKey(BOB_XYM_PRIVATE_KEY))
bob_xym_address = facade.network.public_key_to_address(
    bob_xym_key_pair.public_key)
print(f'Bob Symbol address: {bob_xym_address}')

# Ethereum accounts
w3 = Web3(Web3.HTTPProvider(ETH_RPC_URL))

ALICE_ETH_PRIVATE_KEY = os.getenv('ALICE_ETH_PRIVATE_KEY',
    '0xa73276699ba72dc7b5c9d08deaf8cd88eec33c866341b120304432b89586d45d')
alice_eth_account = w3.eth.account.from_key(ALICE_ETH_PRIVATE_KEY)
print(f'Alice ETH address: {alice_eth_account.address}')

BOB_ETH_PRIVATE_KEY = os.getenv('BOB_ETH_PRIVATE_KEY',
    '0x8e85561005f27d926af79a7ce3e76e75108a09ff2ab78bf65b5578d2e5d509bf')
bob_eth_account = w3.eth.account.from_key(BOB_ETH_PRIVATE_KEY)
print(f'Bob ETH address: {bob_eth_account.address}')

try:
    # --- Alice: Generate proof and hashlock ---
    print('\n--- Alice: Generate proof and hashlock ---')

    proof = os.urandom(32)
    print(f'Proof (hex): {proof.hex()}')

    first_hash = hashlib.sha256(proof).digest()
    secret = hashlib.sha256(first_hash).digest()
    print(f'Secret (double SHA-256): {secret.hex()}')

    # --- Step 1. Alice: Lock ETH on Ethereum ---
    print('\n--- Step 1. Alice: Lock ETH on Ethereum ---')

    htlc = w3.eth.contract(address=HTLC_ADDRESS, abi=HTLC_ABI)
    timelock = int(time.time()) + 72 * 60 * 60
    print(f'Ethereum timelock (Unix): {timelock}')

    lock_call = htlc.functions.newContract(
        bob_eth_account.address, secret, timelock)
    lock_tx = lock_call.build_transaction({
        'from': alice_eth_account.address,
        'value': w3.to_wei(0.01, 'ether'),
        'nonce': w3.eth.get_transaction_count(alice_eth_account.address)
    })
    signed_lock_tx = alice_eth_account.sign_transaction(lock_tx)
    lock_tx_hash = w3.eth.send_raw_transaction(
        signed_lock_tx.raw_transaction)
    print(f'Lock TX hash: {lock_tx_hash.hex()}')

    lock_receipt = w3.eth.wait_for_transaction_receipt(lock_tx_hash)
    print(f'Lock confirmed in block {lock_receipt.blockNumber}')

    # Extract the contractId from the LogHTLCNew event
    contract_id = lock_receipt.logs[0].topics[1]
    print(f'HTLC contract ID: {contract_id.hex()}')

    # --- Step 2. Bob: Create secret lock on Symbol ---
    print('\n--- Step 2. Bob: Create secret lock on Symbol ---')

    # Bob queries the Ethereum contract to get the hashlock
    contract_info = htlc.functions.getContract(contract_id).call()
    hashlock = contract_info[3]  # hashlock field
    print(f'Hashlock from chain: {hashlock.hex()}')

    lock_duration = 5760  # ~48h at 30s blocks
    print(f'Lock duration: {lock_duration} blocks')

    secret_lock_transaction = facade.transaction_factory.create({
        'type': 'secret_lock_transaction_v1',
        'signer_public_key': bob_xym_key_pair.public_key,
        'deadline': get_network_time().add_hours(2).timestamp,
        'recipient_address': alice_xym_address,
        'mosaic': {
            'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
            'amount': 1_000000  # 1 XYM
        },
        'duration': lock_duration,
        'secret': Hash256(hashlock),
        'hash_algorithm': 'hash_256'
    })
    secret_lock_transaction.fee = Amount(
        get_fee_multiplier() * secret_lock_transaction.size)

    # Sign and announce
    lock_signature = facade.sign_transaction(
        bob_xym_key_pair, secret_lock_transaction)
    lock_payload = facade.transaction_factory.attach_signature(
        secret_lock_transaction, lock_signature)

    print('Built secret lock transaction:')
    print(json.dumps(secret_lock_transaction.to_json(), indent=2))

    lock_hash = facade.hash_transaction(secret_lock_transaction)
    print(f'Secret lock transaction hash: {lock_hash}')
    announce_transaction(lock_payload, '/transactions', 'secret lock')
    wait_for_status(lock_hash, 'confirmed', 'Secret lock')

    # --- Step 3. Alice: Claim XYM on Symbol ---
    print('\n--- Step 3. Alice: Claim XYM on Symbol ---')

    secret_proof_transaction = facade.transaction_factory.create({
        'type': 'secret_proof_transaction_v1',
        'signer_public_key': alice_xym_key_pair.public_key,
        'deadline': get_network_time().add_hours(2).timestamp,
        'recipient_address': alice_xym_address,
        'secret': Hash256(hashlock),
        'hash_algorithm': 'hash_256',
        'proof': proof
    })
    secret_proof_transaction.fee = Amount(
        get_fee_multiplier() * secret_proof_transaction.size)

    # Sign and announce
    proof_signature = facade.sign_transaction(
        alice_xym_key_pair, secret_proof_transaction)
    proof_payload = facade.transaction_factory.attach_signature(
        secret_proof_transaction, proof_signature)

    print('Built secret proof transaction:')
    print(json.dumps(secret_proof_transaction.to_json(), indent=2))

    proof_hash = facade.hash_transaction(secret_proof_transaction)
    print(f'Secret proof transaction hash: {proof_hash}')
    announce_transaction(proof_payload, '/transactions', 'secret proof')
    wait_for_status(proof_hash, 'confirmed', 'Secret proof')

    # --- Step 4. Bob: Withdraw ETH on Ethereum ---
    print('\n--- Step 4. Bob: Withdraw ETH on Ethereum ---')

    # Bob waits for Alice to reveal the proof on Symbol.
    revealed_proof = wait_for_secret_proof(alice_xym_address, hashlock)
    print(f'Proof from chain: {revealed_proof.hex()}')

    withdraw_call = htlc.functions.withdraw(contract_id, revealed_proof)
    withdraw_tx = withdraw_call.build_transaction({
        'from': bob_eth_account.address,
        'nonce': w3.eth.get_transaction_count(bob_eth_account.address)
    })
    signed_withdraw_tx = bob_eth_account.sign_transaction(withdraw_tx)
    withdraw_tx_hash = w3.eth.send_raw_transaction(
        signed_withdraw_tx.raw_transaction)
    print(f'Withdraw TX hash: {withdraw_tx_hash.hex()}')

    withdraw_receipt = w3.eth.wait_for_transaction_receipt(
        withdraw_tx_hash)
    print(f'Withdraw confirmed in block {withdraw_receipt.blockNumber}')

    print('\n--- Cross-chain swap complete ---')

except urllib.error.URLError as e:
    print(e.reason)
except Exception as e:
    print(e)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    SymbolFacade,
    NetworkTimestamp,
    models,
    generateMosaicAliasId
} from 'symbol-sdk/symbol';
import { createHash, randomBytes } from 'crypto';
import { ethers } from 'ethers';

const SYMBOL_NODE_URL = process.env.SYMBOL_NODE_URL ||
    'https://reference.symboltest.net:3001';
console.log('Using Symbol node', SYMBOL_NODE_URL);

const ETH_RPC_URL = process.env.ETH_RPC_URL ||
    'https://ethereum-sepolia-rpc.publicnode.com';
console.log('Using Ethereum RPC', ETH_RPC_URL);

// Ethereum HTLC contract on Sepolia
const HTLC_ADDRESS = '0xd58e030bd21c7788897aE5Ea845DaBA936e91D2B';
const HTLC_ABI = [
    'function newContract(address, bytes32, uint) '
        + 'external payable returns (bytes32)',
    'function withdraw(bytes32, bytes) '
        + 'external returns (bool)',
    'function getContract(bytes32) external view '
        + 'returns (address sender, address receiver, '
        + 'uint amount, bytes32 hashlock, '
        + 'uint timelock, bool withdrawn, '
        + 'bool refunded, bytes preimage)',
    'event LogHTLCNew(bytes32 indexed contractId, '
        + 'address indexed sender, '
        + 'address indexed receiver, uint amount, '
        + 'bytes32 hashlock, uint timelock)'
];

// Helper function to fetch current Symbol network time
async function getNetworkTime() {
    const timePath = '/node/time';
    console.log('Fetching current network time from', timePath);
    const timeResponse =
        await fetch(`${SYMBOL_NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const timestamp = new NetworkTimestamp(
        timeJSON.communicationTimestamps.receiveTimestamp);
    console.log('  Network time:', timestamp.timestamp,
        'ms since nemesis');
    return timestamp;
}

// Helper function to fetch recommended Symbol fee multiplier
async function getFeeMultiplier() {
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse =
        await fetch(`${SYMBOL_NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMult = feeJSON.medianFeeMultiplier;
    const minimumMult = feeJSON.minFeeMultiplier;
    const feeMult = Math.max(medianMult, minimumMult);
    console.log('  Fee multiplier:', feeMult);
    return feeMult;
}

// Helper function to announce a Symbol transaction
async function announceTransaction(payload, endpoint, label) {
    console.log(`Announcing ${label} to ${endpoint}`);
    const response = await fetch(
        `${SYMBOL_NODE_URL}${endpoint}`, {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: payload
        });
    console.log('  Response:', await response.text());
}

// Helper function to wait for Symbol transaction status
async function waitForStatus(hash, expectedStatus, label) {
    console.log(
        `Waiting for ${label} to reach ${expectedStatus} status...`);
    let attempts = 0;
    const maxAttempts = 60;

    while (attempts < maxAttempts) {
        try {
            const url = `${SYMBOL_NODE_URL}/transactionStatus/${hash}`;
            const response = await fetch(url);

            if (!response.ok) {
                const error = new Error(
                    `HTTP ${response.status}: ${response.statusText}`);
                error.status = response.status;
                throw error;
            }

            const status = await response.json();
            console.log('  Transaction status:', status.group);

            if (status.group === 'failed') {
                throw new Error(`${label} failed: ${status.code}`);
            }

            if (status.group === expectedStatus) {
                console.log(
                    `${label} ${expectedStatus} in ${attempts} seconds`
                );
                return;
            }
        } catch (error) {
            if (error.status === 404) {
                console.log('  Transaction status: not yet available');
            } else {
                throw error;
            }
        }

        attempts++;
        await new Promise(resolve => setTimeout(resolve, 1000));
    }

    throw new Error(
        `${label} not ${expectedStatus} after ${maxAttempts} attempts`);
}

// Poll Symbol for a confirmed secret proof transaction matching
// a hashlock.
async function waitForSecretProof(signerAddress, hashlock) {
    const hashlockHex = hashlock.toUpperCase();
    const url = `${SYMBOL_NODE_URL}/transactions/confirmed`
        + `?address=${signerAddress}&type=16978&order=desc`;
    console.log(`Polling ${url}`);
    console.log(`  Looking for secret: ${hashlockHex}`);

    let attempts = 0;
    const maxAttempts = 60;
    while (attempts < maxAttempts) {
        const response = await fetch(url);
        const data = await response.json();
        for (const tx of data.data || []) {
            const secret = (tx.transaction.secret || '').toUpperCase();
            if (secret === hashlockHex) {
                console.log(
                    `  Found proof transaction after ${attempts}s`);
                return Buffer.from(tx.transaction.proof, 'hex');
            }
        }
        attempts++;
        await new Promise(resolve => setTimeout(resolve, 1000));
    }

    throw new Error(
        `Secret proof not found after ${maxAttempts} attempts`);
}

// Symbol accounts
const facade = new SymbolFacade('testnet');

// Alice (creates the ETH lock, claims XYM on Symbol)
const ALICE_XYM_PRIVATE_KEY = process.env.ALICE_XYM_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const aliceXymKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(ALICE_XYM_PRIVATE_KEY));
const aliceXymAddress = facade.network.publicKeyToAddress(
    aliceXymKeyPair.publicKey);
console.log('Alice Symbol address:', aliceXymAddress.toString());

// Bob (creates the XYM lock, claims ETH on Ethereum)
const BOB_XYM_PRIVATE_KEY = process.env.BOB_XYM_PRIVATE_KEY ||
    '1111111111111111111111111111111111111111111111111111111111111111';
const bobXymKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(BOB_XYM_PRIVATE_KEY));
const bobXymAddress = facade.network.publicKeyToAddress(
    bobXymKeyPair.publicKey);
console.log('Bob Symbol address:', bobXymAddress.toString());

// Ethereum accounts
const ethProvider = new ethers.JsonRpcProvider(ETH_RPC_URL);

const ALICE_ETH_PRIVATE_KEY = process.env.ALICE_ETH_PRIVATE_KEY ||
    '0xa73276699ba72dc7b5c9d08deaf8cd88eec33c866341b120304432b89586d45d';
const aliceEthWallet = new ethers.Wallet(
    ALICE_ETH_PRIVATE_KEY, ethProvider);
console.log('Alice ETH address:', aliceEthWallet.address);

const BOB_ETH_PRIVATE_KEY = process.env.BOB_ETH_PRIVATE_KEY ||
    '0x8e85561005f27d926af79a7ce3e76e75108a09ff2ab78bf65b5578d2e5d509bf';
const bobEthWallet = new ethers.Wallet(BOB_ETH_PRIVATE_KEY, ethProvider);
console.log('Bob ETH address:', bobEthWallet.address);

try {
    // --- Alice: Generate proof and hashlock ---
    console.log('\n--- Alice: Generate proof and hashlock ---');

    const proof = randomBytes(32);
    console.log('Proof (hex):', proof.toString('hex'));

    const firstHash = createHash('sha256').update(proof).digest();
    const secret = createHash('sha256').update(firstHash).digest();
    console.log('Secret (double SHA-256):', secret.toString('hex'));

    // --- Step 1. Alice: Lock ETH on Ethereum ---
    console.log('\n--- Step 1. Alice: Lock ETH on Ethereum ---');

    const htlcAsAlice = new ethers.Contract(
        HTLC_ADDRESS, HTLC_ABI, aliceEthWallet);

    const timelock = Math.floor(Date.now() / 1000) + 72 * 60 * 60;
    console.log('Ethereum timelock (Unix):', timelock);

    const lockTx = await htlcAsAlice.newContract(
        bobEthWallet.address,
        '0x' + secret.toString('hex'),
        timelock,
        { value: ethers.parseEther('0.01') }
    );
    console.log('Lock TX hash:', lockTx.hash);

    const lockReceipt = await lockTx.wait();
    console.log('Lock confirmed in block', lockReceipt.blockNumber);

    // Extract the contractId from the LogHTLCNew event
    const contractId = lockReceipt.logs[0].topics[1];
    console.log('HTLC contract ID:', contractId);

    // --- Step 2. Bob: Create secret lock on Symbol ---
    console.log('\n--- Step 2. Bob: Create secret lock on Symbol ---');

    // Bob queries the Ethereum contract to get the hashlock
    const htlcAsBob = new ethers.Contract(
        HTLC_ADDRESS, HTLC_ABI, bobEthWallet);
    const contractInfo = await htlcAsBob.getContract(contractId);
    const hashlock = contractInfo.hashlock.slice(2); // strip 0x prefix
    console.log('Hashlock from chain:', hashlock);

    const lockDuration = 5760n; // ~48h at 30s blocks
    console.log('Lock duration:', lockDuration.toString(), 'blocks');

    const secretLockTransaction =
        facade.transactionFactory.create({
            type: 'secret_lock_transaction_v1',
            signerPublicKey: bobXymKeyPair.publicKey.toString(),
            deadline: (await getNetworkTime()).addHours(2).timestamp,
            recipientAddress: aliceXymAddress.toString(),
            mosaic: {
                mosaicId: generateMosaicAliasId('symbol.xym'),
                amount: 1_000000n // 1 XYM
            },
            duration: lockDuration,
            secret: hashlock,
            hashAlgorithm: 'hash_256'
        });
    secretLockTransaction.fee = new models.Amount(
        (await getFeeMultiplier()) * secretLockTransaction.size);

    // Sign and announce
    const lockSignature = facade.signTransaction(
        bobXymKeyPair, secretLockTransaction);
    const lockPayload = facade.transactionFactory.static.attachSignature(
        secretLockTransaction, lockSignature);

    console.log('Built secret lock transaction:');
    console.dir(secretLockTransaction.toJson(), { colors: true });

    const lockHash = facade.hashTransaction(
        secretLockTransaction).toString();
    console.log('Secret lock transaction hash:', lockHash);
    await announceTransaction(lockPayload, '/transactions',
        'secret lock');
    await waitForStatus(lockHash, 'confirmed', 'Secret lock');

    // --- Step 3. Alice: Claim XYM on Symbol ---
    console.log('\n--- Step 3. Alice: Claim XYM on Symbol ---');

    const secretProofTransaction =
        facade.transactionFactory.create({
            type: 'secret_proof_transaction_v1',
            signerPublicKey:
                aliceXymKeyPair.publicKey.toString(),
            deadline: (await getNetworkTime()).addHours(2).timestamp,
            recipientAddress: aliceXymAddress.toString(),
            secret: hashlock,
            hashAlgorithm: 'hash_256',
            proof: proof
        });
    secretProofTransaction.fee = new models.Amount(
        (await getFeeMultiplier()) * secretProofTransaction.size);

    // Sign and announce
    const proofSignature = facade.signTransaction(
        aliceXymKeyPair, secretProofTransaction);
    const proofPayload = facade.transactionFactory.static.attachSignature(
        secretProofTransaction, proofSignature);

    console.log('Built secret proof transaction:');
    console.dir(secretProofTransaction.toJson(), { colors: true });

    const proofHash = facade.hashTransaction(
        secretProofTransaction).toString();
    console.log('Secret proof transaction hash:', proofHash);
    await announceTransaction(
        proofPayload, '/transactions', 'secret proof');
    await waitForStatus(proofHash, 'confirmed', 'Secret proof');

    // --- Step 4. Bob: Withdraw ETH on Ethereum ---
    console.log('\n--- Step 4. Bob: Withdraw ETH on Ethereum ---');

    // Bob waits for Alice to reveal the proof on Symbol.
    const revealedProof = await waitForSecretProof(
        aliceXymAddress.toString(), hashlock);
    console.log('Proof from chain:', revealedProof.toString('hex'));

    const withdrawTx = await htlcAsBob.withdraw(
        contractId, revealedProof);
    console.log('Withdraw TX hash:', withdrawTx.hash);

    const withdrawReceipt = await withdrawTx.wait();
    console.log('Withdraw confirmed in block',
        withdrawReceipt.blockNumber);

    console.log('\n--- Cross-chain swap complete ---');
} catch (e) {
    console.error(e.message);
}

Download source

イーサリアムHTLCコントラクト⚓︎

このチュートリアルでは、Symbolのシークレットロックの相手側として、イーサリアム上にデプロイされたサンプルのHTLCコントラクトを使用します。 コントラクトのソースは hashed-timelock-contract-ethereum リポジトリで入手できます。

教育目的のみ

本番環境で使用されるコントラクトは、タイミングが双方のセキュリティにとって重要であるため、ロックとコントラクトの有効期限を慎重に調整する必要があります。

コントラクトは3つの主要なメソッドを提供します。

  • newContract(address receiver, bytes32 hashlock, uint timelock): 受信者、ハッシュロック、およびタイムロックとしてのUnixタイムスタンプを使用して新しいHTLCを作成します。 Symbolの SecretLockTransactionV1 に相当します。
  • withdraw(bytes32 contractId, bytes proof): 受信者がハッシュロックと一致する証明を提供することで、資金を請求できるようにします。 Symbolの SecretProofTransactionV1 に相当します。
  • refund(bytes32 contractId): タイムロックの期限切れ後に資金を作成者に返金します。 Symbolでは、シークレットロックの期限が切れると自動的に返金が行われます。

コントラクトはSepoliaテストネットのアドレス 0xd58e030bd21c7788897aE5Ea845DaBA936e91D2B にデプロイされています。

コードの解説⚓︎

アリスとボブはそれぞれ両方のチェーンにアカウントを持つ必要があります。アリスはイーサリアムでETHをロックしてSymbolでXYMを請求し、一方ボブはSymbolでXYMをロックしてイーサリアムでETHを請求します。 アリスが開始者です。彼女はランダムな秘密(証明)を生成し、その暗号化ハッシュ(ハッシュロック)を計算し、それを条件としてイーサリアム上で自身のETHをロックします。 次に、ボブは同じハッシュロックを使用してSymbolで自身のXYMをロックします。これにより、証明を公開することでのみ、どちらの側のロックも解除できるようになります。

コードは以下の4つのステップを順番に実行します。

CrossChainSwapStepsA_startA_endA_start->A_endA_labelイーサリアムでのアリスのETHのロック (72時間)B_startB_endB_start->B_endB_labelSymbolでのボブのXYMのロック (48時間)T1_topT1_botT1_top->T1_botT2_topT2_botT2_top->T2_botT3_topT3_botT3_top->T3_botT5_topT5_botT5_top->T5_botL11. アリスがETHをロックするL22. ボブがXYMをロックするL33. アリスがXYMを請求し証明を公開するL54. ボブがETHを請求する

  1. アリスがイーサリアムでETHをロックする: イーサリアムのHTLCコントラクト内で、ハッシュロックによって保護されます。 この時点ではアリスだけが知っている、一致する証明によってのみロックを解除できます。
  2. ボブがSymbolでXYMをロックする: 同じハッシュロックを使用して、 SecretLockTransactionV1 を作成します。
  3. アリスがSymbolでXYMを請求する: SecretProofTransactionV1 を通じて証明を公開することで、証明がSymbol上で誰でも見れる状態になります。
  4. ボブがイーサリアムでETHを請求する: Symbolからアリスの証明を読み取り、イーサリアムHTLCコントラクトの withdraw を呼び出します。

実際には、アリスとボブはそれぞれ別のマシンで自分のパートを実行します。 このチュートリアルでは、分かりやすくするために両方の側を1つのスクリプトにまとめています。

コードでは、転送チュートリアルで説明されているのと同じパターンに従って、ネットワークの時刻や手数料を取得したり、トランザクションをアナウンスしたり、承認をポーリングしたりするためのヘルパー関数を定義しています。

このチュートリアルではステップ間でトランザクションの ファイナリティ を待ちませんが、本番環境への実装ではロールバック関連のリスクを防ぐために必ず待機する必要があります。

アカウントのセットアップ⚓︎

# Symbol accounts
facade = SymbolFacade('testnet')

ALICE_XYM_PRIVATE_KEY = os.getenv('ALICE_XYM_PRIVATE_KEY',
    '0000000000000000000000000000000000000000000000000000000000000000')
alice_xym_key_pair = SymbolFacade.KeyPair(
    PrivateKey(ALICE_XYM_PRIVATE_KEY))
alice_xym_address = facade.network.public_key_to_address(
    alice_xym_key_pair.public_key)
print(f'Alice Symbol address: {alice_xym_address}')

BOB_XYM_PRIVATE_KEY = os.getenv('BOB_XYM_PRIVATE_KEY',
    '1111111111111111111111111111111111111111111111111111111111111111')
bob_xym_key_pair = SymbolFacade.KeyPair(PrivateKey(BOB_XYM_PRIVATE_KEY))
bob_xym_address = facade.network.public_key_to_address(
    bob_xym_key_pair.public_key)
print(f'Bob Symbol address: {bob_xym_address}')

# Ethereum accounts
w3 = Web3(Web3.HTTPProvider(ETH_RPC_URL))

ALICE_ETH_PRIVATE_KEY = os.getenv('ALICE_ETH_PRIVATE_KEY',
    '0xa73276699ba72dc7b5c9d08deaf8cd88eec33c866341b120304432b89586d45d')
alice_eth_account = w3.eth.account.from_key(ALICE_ETH_PRIVATE_KEY)
print(f'Alice ETH address: {alice_eth_account.address}')

BOB_ETH_PRIVATE_KEY = os.getenv('BOB_ETH_PRIVATE_KEY',
    '0x8e85561005f27d926af79a7ce3e76e75108a09ff2ab78bf65b5578d2e5d509bf')
bob_eth_account = w3.eth.account.from_key(BOB_ETH_PRIVATE_KEY)
print(f'Bob ETH address: {bob_eth_account.address}')
// Symbol accounts
const facade = new SymbolFacade('testnet');

// Alice (creates the ETH lock, claims XYM on Symbol)
const ALICE_XYM_PRIVATE_KEY = process.env.ALICE_XYM_PRIVATE_KEY ||
    '0000000000000000000000000000000000000000000000000000000000000000';
const aliceXymKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(ALICE_XYM_PRIVATE_KEY));
const aliceXymAddress = facade.network.publicKeyToAddress(
    aliceXymKeyPair.publicKey);
console.log('Alice Symbol address:', aliceXymAddress.toString());

// Bob (creates the XYM lock, claims ETH on Ethereum)
const BOB_XYM_PRIVATE_KEY = process.env.BOB_XYM_PRIVATE_KEY ||
    '1111111111111111111111111111111111111111111111111111111111111111';
const bobXymKeyPair = new SymbolFacade.KeyPair(
    new PrivateKey(BOB_XYM_PRIVATE_KEY));
const bobXymAddress = facade.network.publicKeyToAddress(
    bobXymKeyPair.publicKey);
console.log('Bob Symbol address:', bobXymAddress.toString());

// Ethereum accounts
const ethProvider = new ethers.JsonRpcProvider(ETH_RPC_URL);

const ALICE_ETH_PRIVATE_KEY = process.env.ALICE_ETH_PRIVATE_KEY ||
    '0xa73276699ba72dc7b5c9d08deaf8cd88eec33c866341b120304432b89586d45d';
const aliceEthWallet = new ethers.Wallet(
    ALICE_ETH_PRIVATE_KEY, ethProvider);
console.log('Alice ETH address:', aliceEthWallet.address);

const BOB_ETH_PRIVATE_KEY = process.env.BOB_ETH_PRIVATE_KEY ||
    '0x8e85561005f27d926af79a7ce3e76e75108a09ff2ab78bf65b5578d2e5d509bf';
const bobEthWallet = new ethers.Wallet(BOB_ETH_PRIVATE_KEY, ethProvider);
console.log('Bob ETH address:', bobEthWallet.address);

ALICE_XYM_PRIVATE_KEYBOB_XYM_PRIVATE_KEY 環境変数はSymbolの鍵を設定し、 ALICE_ETH_PRIVATE_KEYBOB_ETH_PRIVATE_KEY はイーサリアムの鍵を設定します。 便宜上、事前に資金が提供されたテストキーがデフォルトとして提供されていますが、これらは保守されておらず、資金が枯渇する可能性があります。

アリス:証明とハッシュロックの生成⚓︎

    print('\n--- Alice: Generate proof and hashlock ---')

    proof = os.urandom(32)
    print(f'Proof (hex): {proof.hex()}')

    first_hash = hashlib.sha256(proof).digest()
    secret = hashlib.sha256(first_hash).digest()
    print(f'Secret (double SHA-256): {secret.hex()}')
    console.log('\n--- Alice: Generate proof and hashlock ---');

    const proof = randomBytes(32);
    console.log('Proof (hex):', proof.toString('hex'));

    const firstHash = createHash('sha256').update(proof).digest();
    const secret = createHash('sha256').update(firstHash).digest();
    console.log('Secret (double SHA-256):', secret.toString('hex'));

スワップの開始者として、アリスはランダムな32バイトの値を証明として生成します。 次に、ダブルSHA-256を使用してそれをハッシュ化し、ハッシュロックを生成します。

ダブルSHA-256アルゴリズムが選択されているのは、Symbol( hash_256 として)とイーサリアムのHTLCコントラクトの両方でサポートされているためです。 スワップを機能させるには、両方のチェーンで同じアルゴリズムを使用することが不可欠です。

その他のハッシュアルゴリズム

Symbolは、シークレットロック用に他のハッシュアルゴリズムもサポートしています。 利用可能なすべての値については、 LockHashAlgorithm を参照してください。

ステップ1. アリス:イーサリアムでETHをロックする⚓︎

    print('\n--- Step 1. Alice: Lock ETH on Ethereum ---')

    htlc = w3.eth.contract(address=HTLC_ADDRESS, abi=HTLC_ABI)
    timelock = int(time.time()) + 72 * 60 * 60
    print(f'Ethereum timelock (Unix): {timelock}')

    lock_call = htlc.functions.newContract(
        bob_eth_account.address, secret, timelock)
    lock_tx = lock_call.build_transaction({
        'from': alice_eth_account.address,
        'value': w3.to_wei(0.01, 'ether'),
        'nonce': w3.eth.get_transaction_count(alice_eth_account.address)
    })
    signed_lock_tx = alice_eth_account.sign_transaction(lock_tx)
    lock_tx_hash = w3.eth.send_raw_transaction(
        signed_lock_tx.raw_transaction)
    print(f'Lock TX hash: {lock_tx_hash.hex()}')

    lock_receipt = w3.eth.wait_for_transaction_receipt(lock_tx_hash)
    print(f'Lock confirmed in block {lock_receipt.blockNumber}')

    # Extract the contractId from the LogHTLCNew event
    contract_id = lock_receipt.logs[0].topics[1]
    print(f'HTLC contract ID: {contract_id.hex()}')
    console.log('\n--- Step 1. Alice: Lock ETH on Ethereum ---');

    const htlcAsAlice = new ethers.Contract(
        HTLC_ADDRESS, HTLC_ABI, aliceEthWallet);

    const timelock = Math.floor(Date.now() / 1000) + 72 * 60 * 60;
    console.log('Ethereum timelock (Unix):', timelock);

    const lockTx = await htlcAsAlice.newContract(
        bobEthWallet.address,
        '0x' + secret.toString('hex'),
        timelock,
        { value: ethers.parseEther('0.01') }
    );
    console.log('Lock TX hash:', lockTx.hash);

    const lockReceipt = await lockTx.wait();
    console.log('Lock confirmed in block', lockReceipt.blockNumber);

    // Extract the contractId from the LogHTLCNew event
    const contractId = lockReceipt.logs[0].topics[1];
    console.log('HTLC contract ID:', contractId);

アリスはイーサリアムHTLCコントラクトの newContract を呼び出し、ボブのために0.01 ETHをロックします。

  • 受信者: ボブのイーサリアムアドレス。
  • ハッシュロック: 証明の二重SHA-256ハッシュ。この時点ではアリスだけが証明を知っています。
  • タイムロック: ボブがスワップを完了しなかった場合にアリスがETHを回収できるようになる、72時間後のUnixタイムスタンプ。
  • 値: トランザクションとともに送信される0.01 ETH。

トランザクションレシートには、このHTLCを識別する contractId を含む LogHTLCNew イベントが含まれています。 ボブは後でETHを引き出すためにこの contractId が必要になります。

ステップ2. ボブ:Symbolでシークレットロックを作成する⚓︎

    print('\n--- Step 2. Bob: Create secret lock on Symbol ---')

    # Bob queries the Ethereum contract to get the hashlock
    contract_info = htlc.functions.getContract(contract_id).call()
    hashlock = contract_info[3]  # hashlock field
    print(f'Hashlock from chain: {hashlock.hex()}')

    lock_duration = 5760  # ~48h at 30s blocks
    print(f'Lock duration: {lock_duration} blocks')

    secret_lock_transaction = facade.transaction_factory.create({
        'type': 'secret_lock_transaction_v1',
        'signer_public_key': bob_xym_key_pair.public_key,
        'deadline': get_network_time().add_hours(2).timestamp,
        'recipient_address': alice_xym_address,
        'mosaic': {
            'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
            'amount': 1_000000  # 1 XYM
        },
        'duration': lock_duration,
        'secret': Hash256(hashlock),
        'hash_algorithm': 'hash_256'
    })
    secret_lock_transaction.fee = Amount(
        get_fee_multiplier() * secret_lock_transaction.size)

    # Sign and announce
    lock_signature = facade.sign_transaction(
        bob_xym_key_pair, secret_lock_transaction)
    lock_payload = facade.transaction_factory.attach_signature(
        secret_lock_transaction, lock_signature)

    print('Built secret lock transaction:')
    print(json.dumps(secret_lock_transaction.to_json(), indent=2))

    lock_hash = facade.hash_transaction(secret_lock_transaction)
    print(f'Secret lock transaction hash: {lock_hash}')
    announce_transaction(lock_payload, '/transactions', 'secret lock')
    wait_for_status(lock_hash, 'confirmed', 'Secret lock')
    console.log('\n--- Step 2. Bob: Create secret lock on Symbol ---');

    // Bob queries the Ethereum contract to get the hashlock
    const htlcAsBob = new ethers.Contract(
        HTLC_ADDRESS, HTLC_ABI, bobEthWallet);
    const contractInfo = await htlcAsBob.getContract(contractId);
    const hashlock = contractInfo.hashlock.slice(2); // strip 0x prefix
    console.log('Hashlock from chain:', hashlock);

    const lockDuration = 5760n; // ~48h at 30s blocks
    console.log('Lock duration:', lockDuration.toString(), 'blocks');

    const secretLockTransaction =
        facade.transactionFactory.create({
            type: 'secret_lock_transaction_v1',
            signerPublicKey: bobXymKeyPair.publicKey.toString(),
            deadline: (await getNetworkTime()).addHours(2).timestamp,
            recipientAddress: aliceXymAddress.toString(),
            mosaic: {
                mosaicId: generateMosaicAliasId('symbol.xym'),
                amount: 1_000000n // 1 XYM
            },
            duration: lockDuration,
            secret: hashlock,
            hashAlgorithm: 'hash_256'
        });
    secretLockTransaction.fee = new models.Amount(
        (await getFeeMultiplier()) * secretLockTransaction.size);

    // Sign and announce
    const lockSignature = facade.signTransaction(
        bobXymKeyPair, secretLockTransaction);
    const lockPayload = facade.transactionFactory.static.attachSignature(
        secretLockTransaction, lockSignature);

    console.log('Built secret lock transaction:');
    console.dir(secretLockTransaction.toJson(), { colors: true });

    const lockHash = facade.hashTransaction(
        secretLockTransaction).toString();
    console.log('Secret lock transaction hash:', lockHash);
    await announceTransaction(lockPayload, '/transactions',
        'secret lock');
    await waitForStatus(lockHash, 'confirmed', 'Secret lock');

ボブはまず、 getContract を使用してイーサリアムHTLCコントラクトをクエリし、アリスが使用したハッシュロックを取得します。

ロック前の検証

ボブは自身の資金をロックする前に、コントラクトの詳細すべて(金額、受信者、タイムロック)を検証する必要があります。 このチュートリアルでは、簡単にするためにハッシュロックのみを読み取っています。

次に、ボブはSymbolで SecretLockTransactionV1 を作成し、同じハッシュロックを使用してアリスのために1 XYMをロックします。

  • 受信者: アリスのSymbolアドレス。
  • モザイク: 1 XYM(可分性6の 1_000000 アトミック単位として表現)。
  • 期間: 5760ブロック(30秒のブロック時間で約48時間)。

    タイムロックの順序

    この期間は、アリスの72時間のイーサリアムタイムロックより短くなければなりません。 そうしないと、アリスは自分のETHを返金しても、ボブのXYMを請求できる可能性があります。 2つの間のギャップは十分に大きくなければなりません。これは、アリスがぎりぎりで証明を公開した場合でも、ボブがイーサリアムで引き出すことを可能にする安全マージンです。 安全上の考慮事項を参照してください。

  • ハッシュロック( secret フィールド): イーサリアムのコントラクトから取得したハッシュロック。

  • ハッシュアルゴリズム (Hash algorithm): hash_256 (二重SHA-256)。もう一方のチェーンのHTLCで使用されているアルゴリズムと一致する必要があります。

ステップ3. アリス:SymbolでXYMを請求する⚓︎

    print('\n--- Step 3. Alice: Claim XYM on Symbol ---')

    secret_proof_transaction = facade.transaction_factory.create({
        'type': 'secret_proof_transaction_v1',
        'signer_public_key': alice_xym_key_pair.public_key,
        'deadline': get_network_time().add_hours(2).timestamp,
        'recipient_address': alice_xym_address,
        'secret': Hash256(hashlock),
        'hash_algorithm': 'hash_256',
        'proof': proof
    })
    secret_proof_transaction.fee = Amount(
        get_fee_multiplier() * secret_proof_transaction.size)

    # Sign and announce
    proof_signature = facade.sign_transaction(
        alice_xym_key_pair, secret_proof_transaction)
    proof_payload = facade.transaction_factory.attach_signature(
        secret_proof_transaction, proof_signature)

    print('Built secret proof transaction:')
    print(json.dumps(secret_proof_transaction.to_json(), indent=2))

    proof_hash = facade.hash_transaction(secret_proof_transaction)
    print(f'Secret proof transaction hash: {proof_hash}')
    announce_transaction(proof_payload, '/transactions', 'secret proof')
    wait_for_status(proof_hash, 'confirmed', 'Secret proof')
    console.log('\n--- Step 3. Alice: Claim XYM on Symbol ---');

    const secretProofTransaction =
        facade.transactionFactory.create({
            type: 'secret_proof_transaction_v1',
            signerPublicKey:
                aliceXymKeyPair.publicKey.toString(),
            deadline: (await getNetworkTime()).addHours(2).timestamp,
            recipientAddress: aliceXymAddress.toString(),
            secret: hashlock,
            hashAlgorithm: 'hash_256',
            proof: proof
        });
    secretProofTransaction.fee = new models.Amount(
        (await getFeeMultiplier()) * secretProofTransaction.size);

    // Sign and announce
    const proofSignature = facade.signTransaction(
        aliceXymKeyPair, secretProofTransaction);
    const proofPayload = facade.transactionFactory.static.attachSignature(
        secretProofTransaction, proofSignature);

    console.log('Built secret proof transaction:');
    console.dir(secretProofTransaction.toJson(), { colors: true });

    const proofHash = facade.hashTransaction(
        secretProofTransaction).toString();
    console.log('Secret proof transaction hash:', proofHash);
    await announceTransaction(
        proofPayload, '/transactions', 'secret proof');
    await waitForStatus(proofHash, 'confirmed', 'Secret proof');

ボブのシークレットロックが承認され、それが予想される金額、ハッシュロック、受信者、およびタイムロックと一致することをアリスが検証したら、彼女は証明を公開することによってSymbol上でロックされたXYMを請求します。

彼女は以下を使用して SecretProofTransactionV1 を作成します。

  • 受信者: アリス自身のSymbolアドレス(ボブのシークレットロックで設定されたのと同じアドレス)。
  • ハッシュロック( secret フィールド): シークレットロックで使用されたのと同じハッシュロック。
  • ハッシュアルゴリズム: hash_256 (シークレットロックと一致する必要があります)。
  • 証明: アリスが生成した元のランダムなバイト。

このトランザクションがアナウンスされて承認されると、アリスはボブがロックしていた1 XYMを受け取り、証明はSymbolブロックチェーン上で誰でも見れる状態になります。 ボブ(または誰でも)はトランザクションデータからそれを読み取れます。

ステップ4. ボブ:イーサリアムでETHを引き出す⚓︎

    print('\n--- Step 4. Bob: Withdraw ETH on Ethereum ---')

    # Bob waits for Alice to reveal the proof on Symbol.
    revealed_proof = wait_for_secret_proof(alice_xym_address, hashlock)
    print(f'Proof from chain: {revealed_proof.hex()}')

    withdraw_call = htlc.functions.withdraw(contract_id, revealed_proof)
    withdraw_tx = withdraw_call.build_transaction({
        'from': bob_eth_account.address,
        'nonce': w3.eth.get_transaction_count(bob_eth_account.address)
    })
    signed_withdraw_tx = bob_eth_account.sign_transaction(withdraw_tx)
    withdraw_tx_hash = w3.eth.send_raw_transaction(
        signed_withdraw_tx.raw_transaction)
    print(f'Withdraw TX hash: {withdraw_tx_hash.hex()}')

    withdraw_receipt = w3.eth.wait_for_transaction_receipt(
        withdraw_tx_hash)
    print(f'Withdraw confirmed in block {withdraw_receipt.blockNumber}')
    console.log('\n--- Step 4. Bob: Withdraw ETH on Ethereum ---');

    // Bob waits for Alice to reveal the proof on Symbol.
    const revealedProof = await waitForSecretProof(
        aliceXymAddress.toString(), hashlock);
    console.log('Proof from chain:', revealedProof.toString('hex'));

    const withdrawTx = await htlcAsBob.withdraw(
        contractId, revealedProof);
    console.log('Withdraw TX hash:', withdrawTx.hash);

    const withdrawReceipt = await withdrawTx.wait();
    console.log('Withdraw confirmed in block',
        withdrawReceipt.blockNumber);

ボブは、アリスからトランザクションハッシュをもらう必要なしに、オンチェーンでアリスの証明を発見します。

wait_for_secret_proof ヘルパーは、アリスのアドレスと type=16978 (SecretProofTransactionV1) でフィルタリングされた /transactions/confirmed GET エンドポイントをポーリングし、 transaction.secret をボブ自身のハッシュロックと照合して正しいエントリを選び出し、そこから transaction.proof を読み取ります。

ハッシュロックは各スワップに固有の32バイトのランダムなバイトであるため、過去にアリスが他のシークレット証明を投稿していたとしても、このスワップの証明トランザクションのみが一致します。

証明が取得されると、ボブはイーサリアムHTLCコントラクトの withdraw を2つの引数とともに呼び出します。

  • コントラクトID: アリスがETHをロックしたときに発行された LogHTLCNew イベントからのHTLC識別子。
  • 証明: アリスがSymbol上で公開した証明。

引き出し期限

ボブは、アリスのイーサリアムタイムロックが期限切れになる前にこのステップを完了する必要があります。 期限が切れると、アリスはイーサリアムのコントラクトで refund を呼び出してETHを回収できてしまいます。

このイーサリアムトランザクションが承認されると、ボブはアリスの0.01 ETHを受け取り、スワップが完了します。 アリスはステップ3の終わりにすでにボブの1 XYMを受け取っています。

出力⚓︎

以下に示す出力は、プログラムの一般的な実行例に対応しています。

Using Symbol node https://reference.symboltest.net:3001
Using Ethereum RPC https://ethereum-sepolia-rpc.publicnode.com
Alice Symbol address: TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I
Bob Symbol address: TCWYXKVYBMO4NBCUF3AXKJMXCGVSYQOS7ZG2TLI
Alice ETH address: 0x8019119CD3f852B65820F0Cf0d7FA0957BEd23E2
Bob ETH address: 0xa5507aa9a7080d5916ABab889876A0910d1Ac328

--- Alice: Generate proof and hashlock ---
Proof (hex): 21e96d2665bd5a063d03656eb202f0bc0eb5bdef06d4c1a34db1f563af99fedb
Secret (double SHA-256): d128c3c3924ecff0a19f81c89a84e7fe6320b8a2b07ff22812e58a2360e1a910

--- Step 1. Alice: Lock ETH on Ethereum ---
Ethereum timelock (Unix): 1776935008
Lock TX hash: 2c198264968661cd560b9ad7bc0f9e6dc8d6d7fa5caaa3bf787fee66138f3e5f
Lock confirmed in block 10696157
HTLC contract ID: 92217fcd48907e5759e5431d8a0d606e64942cdb209d329590a68250a01b41f1

--- Step 2. Bob: Create secret lock on Symbol ---
Hashlock from chain: d128c3c3924ecff0a19f81c89a84e7fe6320b8a2b07ff22812e58a2360e1a910
Lock duration: 5760 blocks
Fetching current network time from /node/time
  Network time: 109425352582 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Built secret lock transaction:
{
  "signature": "9ABE4BE7A02170FEB3E0F4A3FB551D5FB4ECE28F0E02F9FEAF9CBF58CFE491595CB1AB22B161C77C2600FC6F31CF7863C63090A3820405E570854680BA1AF602",
  "signer_public_key": "D04AB232742BB4AB3A1368BD4615E4E6D0224AB71A016BAF8520A332C9778737",
  "version": 1,
  "network": 152,
  "type": 16722,
  "fee": "20900",
  "deadline": "109432552582",
  "recipient_address": "988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE9",
  "secret": "D128C3C3924ECFF0A19F81C89A84E7FE6320B8A2B07FF22812E58A2360E1A910",
  "mosaic": {
    "mosaic_id": "16666583871264174062",
    "amount": "1000000"
  },
  "duration": "5760",
  "hash_algorithm": 2
}
Secret lock transaction hash: 556BD67C7FB3ECE23AAD2C911224350DFB082748BBBD8D683DCE4AF7D2E4BE3A
Announcing secret lock to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for Secret lock to reach confirmed status...
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
Secret lock confirmed in 24 seconds

--- Step 3. Alice: Claim XYM on Symbol ---
Fetching current network time from /node/time
  Network time: 109425393033 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Built secret proof transaction:
{
  "signature": "A5BD627A1A466ABD62D1B7DA60C5E31337DD196A01CFFF417A9DFF04A45E1F9DE3AC7A3ED67054C7CFC2D0FF3E91ACDA225D6F0A94465C4ED39DF684D7B8480C",
  "signer_public_key": "3B6A27BCCEB6A42D62A3A8D02A6F0D73653215771DE243A63AC048A18B59DA29",
  "version": 1,
  "network": 152,
  "type": 16978,
  "fee": "21900",
  "deadline": "109432593033",
  "recipient_address": "988E1191A25A88142C2FB3F69787576E3DC713EFC1CE4DE9",
  "secret": "D128C3C3924ECFF0A19F81C89A84E7FE6320B8A2B07FF22812E58A2360E1A910",
  "hash_algorithm": 2,
  "proof": "21e96d2665bd5a063d03656eb202f0bc0eb5bdef06d4c1a34db1f563af99fedb"
}
Secret proof transaction hash: 1F875BF8074ADB3D40541CD44ABB2088D5588E6CE2F40CE38C4444258440CA9A
Announcing secret proof to /transactions
  Response: {"message":"packet 9 was pushed to the network via /transactions"}
Waiting for Secret proof to reach confirmed status...
  Transaction status: not yet available
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: unconfirmed
  Transaction status: confirmed
Secret proof confirmed in 22 seconds

--- Step 4. Bob: Withdraw ETH on Ethereum ---
Polling https://reference.symboltest.net:3001/transactions/confirmed?address=TCHBDENCLKEBILBPWP3JPB2XNY64OE7PYHHE32I&type=16978&order=desc
  Looking for secret: D128C3C3924ECFF0A19F81C89A84E7FE6320B8A2B07FF22812E58A2360E1A910
  Found proof transaction after 0s
Proof from chain: 21e96d2665bd5a063d03656eb202f0bc0eb5bdef06d4c1a34db1f563af99fedb
Withdraw TX hash: 2a2feb2ce493f1a0800442e45cf3474bd0ab80b16d94f7146accf72c43195d64
Withdraw confirmed in block 10696163

--- Cross-chain swap complete ---

出力の重要なポイント:

  • 行 9-10: アリスは証明とハッシュロックを生成します。証明は、アリスが公開するまで秘密にしておく必要があります。
  • 行 15: イーサリアムでのアリスのETHロックが承認されます。
  • 行 16: HTLCコントラクトIDは、アリスのイーサリアムロックを識別します。ボブはこれを使用してハッシュロックをクエリし、後で引き出します。
  • 行 19: ボブは getContract を使用して、イーサリアムコントラクトからハッシュロックを取得します。
  • 行 50: ボブのSymbolシークレットロックが承認されます。これで、アリスはXYMを請求できるようになります。
  • 行 70: アリスはシークレット証明トランザクションに証明を含めます。アナウンスされると、Symbol上で誰でも見れる状態(パブリック)になります。
  • 行 80: アリスのシークレット証明が承認されます。アリスは1 XYMを受け取ります。
  • 行 87: ボブは、Symbol上で承認されたアリスのトランザクションから公開された証明を取得し、それを使用してイーサリアムで引き出しを行います。
  • 行 89: ボブのイーサリアムでの引き出しが承認されます。 ボブはアリスの0.01 ETHを受け取り、スワップが完了します。

出力に表示されたハッシュを使用して、各ネットワークのブロックエクスプローラーでトランザクションを確認できます。

  • イーサリアム: ロックおよび引き出しトランザクション用の Sepolia Etherscan
  • Symbol: シークレットロックおよびシークレットプルーフトランザクション用の Symbol Testnet Explorer

結論⚓︎

このチュートリアルでは、以下の方法を示しました。

ステップ 関連ドキュメント
証明とハッシュロックの生成 LockHashAlgorithm
イーサリアムでETHをロックする イーサリアムHTLCコントラクト
Symbolでシークレットロックを作成する SecretLockTransactionV1
Symbolで証明を公開する SecretProofTransactionV1
イーサリアムでETHを引き出す イーサリアムHTLCコントラクト

次のステップ⚓︎

このチュートリアルは簡略化された例です。 本番環境でクロスチェーンスワップを使用する前に、テキストブックの安全上の考慮事項を確認してください。