コンテンツにスキップ

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
    

    org.web3j:core は、jbang によりコードアノテーションから自動的にインストールされます。

完全なコード⚓︎

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

import hashlib
import json
import os
import secrets
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 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 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_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')
        return fee_multiplier


# 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 confirmation
def wait_for_confirmation(hash_value, label):
    print(f'Waiting for {label} confirmation...')
    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 RuntimeError(
                        f'{label} failed: {status["code"]}')

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

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

        attempts += 1
        time.sleep(1)

    raise TimeoutError(
        f'{label} not confirmed after {max_attempts} attempts')


# Poll Symbol for a confirmed secret proof transaction matching
# a hashlock.
def wait_for_secret_proof(signer_address, hlock):
    hashlock_hex = hlock.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 trans in data.get('data', []):
            found_secret = trans['transaction'].get('secret', '')
            if found_secret.upper() == hashlock_hex:
                print(f'  Found proof transaction after {attempts}s')
                return bytes.fromhex(trans['transaction']['proof'])
        attempts += 1
        time.sleep(1)

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


# Symbol accounts
facade = SymbolFacade('testnet')

# Alice (creates the ETH lock, claims XYM on Symbol)
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 (creates the XYM lock, claims ETH on Ethereum)
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 = secrets.token_bytes(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.create_transaction_from_descriptor(
        {
            'type': 'secret_lock_transaction_v1',
            'recipient_address': alice_xym_address,
            'secret': Hash256(hashlock),
            'mosaic': {
                'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
                'amount': 1_000_000  # 1 XYM
            },
            'duration': lock_duration,
            'hash_algorithm': 'hash_256'
        },
        bob_xym_key_pair.public_key,
        get_fee_multiplier(),
        2 * 60 * 60)

    # 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_confirmation(lock_hash, 'Secret lock')

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

    secret_proof_transaction = facade.create_transaction_from_descriptor(
        {
            'type': 'secret_proof_transaction_v1',
            'recipient_address': alice_xym_address,
            'secret': Hash256(hashlock),
            'hash_algorithm': 'hash_256',
            'proof': proof
        },
        alice_xym_key_pair.public_key,
        get_fee_multiplier(),
        2 * 60 * 60)

    # 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_confirmation(proof_hash, '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

// eslint-disable-next-line import/no-unresolved
import { ethers } from 'ethers';
import { Hash256, PrivateKey } from 'symbol-sdk';
import {
    SymbolFacade,
    descriptors,
    generateMosaicAliasId,
    models
} from 'symbol-sdk/symbol';
import { createHash, randomBytes } from 'crypto';

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 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 medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);
    return feeMultiplier;
}

// 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 confirmation
async function waitForConfirmation(hash, label) {
    console.log(`Waiting for ${label} confirmation...`);
    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 ('failed' === status.group)
                throw new Error(`${label} failed: ${status.code}`);

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

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

    throw new Error(
        `${label} not confirmed 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.createTransactionFromTypedDescriptor(
            new descriptors.SecretLockTransactionV1Descriptor(
                aliceXymAddress,
                new Hash256(hashlock),
                new descriptors.UnresolvedMosaicDescriptor(
                    generateMosaicAliasId('symbol.xym'),
                    new models.Amount(1_000_000n) // 1 XYM
                ),
                new models.BlockDuration(lockDuration),
                models.LockHashAlgorithm.HASH_256
            ),
            bobXymKeyPair.publicKey,
            await getFeeMultiplier(),
            2 * 60 * 60);

    // 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 waitForConfirmation(lockHash, 'Secret lock');

    // --- Step 3. Alice: Claim XYM on Symbol ---

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

    const secretProofTransaction =
        facade.createTransactionFromTypedDescriptor(
            new descriptors.SecretProofTransactionV1Descriptor(
                aliceXymAddress,
                new Hash256(hashlock),
                models.LockHashAlgorithm.HASH_256,
                proof
            ),
            aliceXymKeyPair.publicKey,
            await getFeeMultiplier(),
            2 * 60 * 60);

    // 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 waitForConfirmation(proofHash, '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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
//JAVA 21+
//DEPS org.web3j:core:4.12.3
//DEPS org.symbol:symbol-sdk:3.3.1

import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HexFormat;
import java.util.List;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.web3j.abi.FunctionEncoder;
import org.web3j.abi.FunctionReturnDecoder;
import org.web3j.abi.TypeReference;
import org.web3j.abi.datatypes.Bool;
import org.web3j.abi.datatypes.DynamicBytes;
import org.web3j.abi.datatypes.Function;
import org.web3j.abi.datatypes.generated.Bytes32;
import org.web3j.abi.datatypes.generated.Uint256;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.response.EthCall;
import org.web3j.protocol.core.methods.response.TransactionReceipt;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.RawTransactionManager;
import org.web3j.tx.response.PollingTransactionReceiptProcessor;
import org.web3j.utils.Convert;
import org.web3j.utils.Numeric;

import org.symbol.sdk.CryptoTypes;
import org.symbol.sdk.facade.SymbolFacade;
import org.symbol.sdk.symbol.Address;
import org.symbol.sdk.symbol.IdGenerator;
import org.symbol.sdk.symbol.KeyPair;
import org.symbol.sdk.symbol.SymbolTransactionFactory;
import org.symbol.sdk.symbol.descriptors.*;
import org.symbol.sdk.symbol.models.*;
import org.symbol.sdk.utils.Converter;

public final class CrossChainSwap {
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

    private static final HttpClient HTTP_CLIENT =
        HttpClient.newHttpClient();

    private static final SecureRandom RANDOM = new SecureRandom();

    private final String symbolNodeUrl = System.getenv().getOrDefault(
        "SYMBOL_NODE_URL", "https://reference.symboltest.net:3001");

    private final String ethRpcUrl = System.getenv().getOrDefault(
        "ETH_RPC_URL", "https://ethereum-sepolia-rpc.publicnode.com");

    // Ethereum HTLC contract on Sepolia
    private static final String HTLC_ADDRESS =
        "0xd58e030bd21c7788897aE5Ea845DaBA936e91D2B";

    private final SymbolFacade facade = new SymbolFacade("testnet");

    private Web3j ethProvider;

    private Credentials aliceEthWallet;

    private Credentials bobEthWallet;

    private Address aliceXymAddress;

    private Address bobXymAddress;

    private KeyPair aliceXymKeyPair;

    private KeyPair bobXymKeyPair;

    public static void main(final String[] args) {
        try {
            new CrossChainSwap().run();
        } catch (final Exception ex) {
            System.out.println(null == ex.getMessage()
                ? ex.toString()
                : ex.getMessage());
        }
    }

    private void run()
        throws Exception {
        System.out.printf("Using Symbol node %s%n", symbolNodeUrl);
        System.out.printf("Using Ethereum RPC %s%n", ethRpcUrl);

        // Symbol accounts
        // Alice (creates the ETH lock, claims XYM on Symbol)
        final String aliceXymPrivateKey = System.getenv().getOrDefault(
            "ALICE_XYM_PRIVATE_KEY", "0".repeat(64));
        aliceXymKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(aliceXymPrivateKey));
        aliceXymAddress = facade.network.publicKeyToAddress(
            aliceXymKeyPair.getPublicKey());
        System.out.printf("Alice Symbol address: %s%n", aliceXymAddress);

        // Bob (creates the XYM lock, claims ETH on Ethereum)
        final String bobXymPrivateKey = System.getenv().getOrDefault(
            "BOB_XYM_PRIVATE_KEY", "1".repeat(64));
        bobXymKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(bobXymPrivateKey));
        bobXymAddress = facade.network.publicKeyToAddress(
            bobXymKeyPair.getPublicKey());
        System.out.printf("Bob Symbol address: %s%n", bobXymAddress);

        // Ethereum accounts
        ethProvider = Web3j.build(new HttpService(ethRpcUrl));

        final String aliceEthPrivateKey = System.getenv().getOrDefault(
            "ALICE_ETH_PRIVATE_KEY",
            "a73276699ba72dc7b5c9d08deaf8cd88eec33c866341b12030443"
                + "2b89586d45d");
        aliceEthWallet = Credentials.create(aliceEthPrivateKey);
        System.out.printf("Alice ETH address: %s%n",
            aliceEthWallet.getAddress());

        final String bobEthPrivateKey = System.getenv().getOrDefault(
            "BOB_ETH_PRIVATE_KEY",
            "8e85561005f27d926af79a7ce3e76e75108a09ff2ab78bf65b557"
                + "8d2e5d509bf");
        bobEthWallet = Credentials.create(bobEthPrivateKey);
        System.out.printf("Bob ETH address: %s%n",
            bobEthWallet.getAddress());


        // --- Alice: Generate proof and hashlock ---
        System.out.println(
            "\n--- Alice: Generate proof and hashlock ---");

        final byte[] proof = new byte[32];
        RANDOM.nextBytes(proof);
        System.out.printf("Proof (hex): %s%n",
            HexFormat.of().formatHex(proof));

        final byte[] firstHash = sha256(proof);
        final byte[] secret = sha256(firstHash);
        System.out.printf("Secret (double SHA-256): %s%n",
            HexFormat.of().formatHex(secret));

        // --- Step 1. Alice: Lock ETH on Ethereum ---
        System.out.println(
            "\n--- Step 1. Alice: Lock ETH on Ethereum ---");

        final long timelock =
            System.currentTimeMillis() / 1000 + (72 * 60 * 60);
        System.out.printf("Ethereum timelock (Unix): %d%n", timelock);

        final String lockHash = sendNewContract(
            bobEthWallet.getAddress(),
            secret,
            BigInteger.valueOf(timelock));
        System.out.printf("Lock TX hash: %s%n", lockHash);

        final TransactionReceipt lockReceipt = waitForEthereumReceipt(
            lockHash);
        System.out.printf("Lock confirmed in block %s%n",
            lockReceipt.getBlockNumber());

        // Extract the contractId from the LogHTLCNew event
        final String contractId = lockReceipt.getLogs().get(0)
            .getTopics().get(1);
        System.out.printf("HTLC contract ID: %s%n", contractId);

        // --- Step 2. Bob: Create secret lock on Symbol ---
        System.out.println(
            "\n--- Step 2. Bob: Create secret lock on Symbol ---");

        // Bob queries the Ethereum contract to get the hashlock
        final String hashlock = getContractHashlock(contractId);
        System.out.printf("Hashlock from chain: %s%n", hashlock);

        final long lockDuration = 5760; // ~48h at 30s blocks
        System.out.printf("Lock duration: %d blocks%n", lockDuration);

        final Transaction secretLockTransaction =
            facade.createTransactionFromTypedDescriptor(
                new SecretLockTransactionV1Descriptor(
                    aliceXymAddress,
                    new CryptoTypes.Hash256(hashlock),
                    new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.xym")),
                        new Amount(1_000_000)), // 1 XYM
                    new BlockDuration(lockDuration),
                    LockHashAlgorithm.HASH_256),
                bobXymKeyPair.getPublicKey(),
                getFeeMultiplier(),
                2 * 60 * 60);

        // Sign and announce
        final CryptoTypes.Signature lockSignature =
            facade.signTransaction(bobXymKeyPair, secretLockTransaction);
        final String lockPayload = SymbolTransactionFactory
            .attachSignature(secretLockTransaction, lockSignature);

        System.out.println("Built secret lock transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(secretLockTransaction.toJson()));

        final String symbolLockHash = facade.hashTransaction(
            secretLockTransaction).toString();
        System.out.printf("Secret lock transaction hash: %s%n",
            symbolLockHash);
        announceTransaction(lockPayload, "/transactions", "secret lock");
        waitForConfirmation(symbolLockHash, "Secret lock");

        // --- Step 3. Alice: Claim XYM on Symbol ---
        System.out.println(
            "\n--- Step 3. Alice: Claim XYM on Symbol ---");

        final Transaction secretProofTransaction =
            facade.createTransactionFromTypedDescriptor(
                new SecretProofTransactionV1Descriptor(
                    aliceXymAddress,
                    new CryptoTypes.Hash256(hashlock),
                    LockHashAlgorithm.HASH_256,
                    proof),
                aliceXymKeyPair.getPublicKey(),
                getFeeMultiplier(),
                2 * 60 * 60);

        // Sign and announce
        final CryptoTypes.Signature proofSignature =
            facade.signTransaction(
                aliceXymKeyPair, secretProofTransaction);
        final String proofPayload = SymbolTransactionFactory
            .attachSignature(secretProofTransaction, proofSignature);

        System.out.println("Built secret proof transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(secretProofTransaction.toJson()));

        final String proofHash = facade.hashTransaction(
            secretProofTransaction).toString();
        System.out.printf("Secret proof transaction hash: %s%n",
            proofHash);
        announceTransaction(proofPayload, "/transactions", "secret proof");
        waitForConfirmation(proofHash, "Secret proof");

        // --- Step 4. Bob: Withdraw ETH on Ethereum ---
        System.out.println(
            "\n--- Step 4. Bob: Withdraw ETH on Ethereum ---");

        // Bob waits for Alice to reveal the proof on Symbol.
        final byte[] revealedProof = waitForSecretProof(
            aliceXymAddress.toString(), hashlock);
        System.out.printf("Proof from chain: %s%n",
            HexFormat.of().formatHex(revealedProof));

        final String withdrawHash = sendWithdraw(
            contractId, revealedProof);
        System.out.printf("Withdraw TX hash: %s%n", withdrawHash);

        final TransactionReceipt withdrawReceipt = waitForEthereumReceipt(
            withdrawHash);
        System.out.printf("Withdraw confirmed in block %s%n",
            withdrawReceipt.getBlockNumber());

        System.out.println("\n--- Cross-chain swap complete ---");
    }

    // Helper function to fetch recommended Symbol fee multiplier
    private long getFeeMultiplier()
        throws IOException, InterruptedException {
        final String feePath = "/network/fees/transaction";
        System.out.printf("Fetching recommended fees from %s%n", feePath);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(symbolNodeUrl + feePath)).GET().build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        final JsonNode feeJSON = JSON_MAPPER.readTree(response.body());
        final long medianMultiplier =
            feeJSON.get("medianFeeMultiplier").asLong();
        final long minimumMultiplier =
            feeJSON.get("minFeeMultiplier").asLong();
        final long feeMultiplier = Math.max(
            medianMultiplier, minimumMultiplier);
        System.out.printf("  Fee multiplier: %d%n", feeMultiplier);
        return feeMultiplier;
    }

    // Helper function to announce a Symbol transaction
    private void announceTransaction(
        final String payload,
        final String endpoint,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Announcing %s to %s%n", label, endpoint);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(symbolNodeUrl + endpoint))
            .header("Content-Type", "application/json")
            .PUT(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        System.out.printf("  Response: %s%n", response.body());
    }

    // Helper function to wait for Symbol transaction confirmation
    private void waitForConfirmation(
        final String hash,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Waiting for %s confirmation...%n", label);
        int attempts = 0;
        final int maxAttempts = 60;

        while (attempts < maxAttempts) {
            final String url = symbolNodeUrl + "/transactionStatus/"
                + hash;
            final HttpRequest request = HttpRequest.newBuilder(
                URI.create(url)).GET().build();
            final HttpResponse<String> response = HTTP_CLIENT.send(
                request, BodyHandlers.ofString());

            if (response.statusCode() / 100 != 2) {
                if (404 == response.statusCode()) {
                    System.out.println(
                        "  Transaction status: not yet available");
                } else
                    throw new IOException(
                        "HTTP " + response.statusCode());
            } else {
                final JsonNode status = JSON_MAPPER.readTree(
                    response.body());
                System.out.printf("  Transaction status: %s%n",
                    status.get("group").asText());

                if ("failed".equals(status.get("group").asText()))
                    throw new IOException(String.format("%s failed: %s",
                        label, status.get("code").asText()));

                if ("confirmed".equals(status.get("group").asText())) {
                    System.out.printf("%s confirmed in %d seconds%n",
                        label, attempts);
                    return;
                }
            }

            ++attempts;
            Thread.sleep(1000);
        }

        throw new IOException(String.format(
            "%s not confirmed after %d attempts", label, maxAttempts));
    }

    // Poll Symbol for a confirmed secret proof transaction matching
    // a hashlock.
    private byte[] waitForSecretProof(
        final String signerAddress,
        final String hashlock
    ) throws IOException, InterruptedException {
        final String hashlockHex = hashlock.toUpperCase();
        final String url = symbolNodeUrl + "/transactions/confirmed"
            + "?address=" + signerAddress + "&type=16978&order=desc";
        System.out.printf("Polling %s%n", url);
        System.out.printf("  Looking for secret: %s%n", hashlockHex);

        int attempts = 0;
        final int maxAttempts = 60;
        while (attempts < maxAttempts) {
            final HttpRequest request = HttpRequest.newBuilder(
                URI.create(url)).GET().build();
            final HttpResponse<String> response = HTTP_CLIENT.send(
                request, BodyHandlers.ofString());
            final JsonNode data = JSON_MAPPER.readTree(response.body());
            for (final JsonNode tx : data.get("data")) {
                final String secret = tx.get("transaction")
                    .get("secret").asText("").toUpperCase();
                if (secret.equals(hashlockHex)) {
                    System.out.printf(
                        "  Found proof transaction after %ds%n",
                        attempts);
                    return Converter.hexToUint8(tx.get("transaction")
                        .get("proof").asText());
                }
            }
            ++attempts;
            Thread.sleep(1000);
        }

        throw new IOException(String.format(
            "Secret proof not found after %d attempts", maxAttempts));
    }

    private String sendNewContract(
        final String receiver,
        final byte[] hashlock,
        final BigInteger timelock
    ) throws Exception {
        final Function function = new Function(
            "newContract",
            List.of(
                new org.web3j.abi.datatypes.Address(receiver),
                new Bytes32(hashlock),
                new Uint256(timelock)),
            List.of(new TypeReference<Bytes32>() {})
        );
        final String data = FunctionEncoder.encode(function);
        final RawTransactionManager manager = new RawTransactionManager(
            ethProvider, aliceEthWallet);
        return manager.sendTransaction(
            BigInteger.valueOf(20_000_000_000L),
            BigInteger.valueOf(300_000),
            HTLC_ADDRESS,
            data,
            Convert.toWei("0.01", Convert.Unit.ETHER).toBigInteger()
        ).getTransactionHash();
    }

    private String getContractHashlock(
        final String contractId
    ) throws IOException {
        final Function function = new Function(
            "getContract",
            List.of(new Bytes32(Numeric.hexStringToByteArray(contractId))),
            Arrays.asList(
                new TypeReference<org.web3j.abi.datatypes.Address>() {},
                new TypeReference<org.web3j.abi.datatypes.Address>() {},
                new TypeReference<Uint256>() {},
                new TypeReference<Bytes32>() {},
                new TypeReference<Uint256>() {},
                new TypeReference<Bool>() {},
                new TypeReference<Bool>() {},
                new TypeReference<DynamicBytes>() {})
        );
        final String data = FunctionEncoder.encode(function);
        final EthCall response = ethProvider.ethCall(
            org.web3j.protocol.core.methods.request.Transaction
                .createEthCallTransaction(
                    bobEthWallet.getAddress(), HTLC_ADDRESS, data),
            DefaultBlockParameterName.LATEST
        ).send();
        return Numeric.toHexStringNoPrefix((byte[]) FunctionReturnDecoder
            .decode(response.getValue(), function.getOutputParameters())
            .get(3).getValue());
    }

    private String sendWithdraw(
        final String contractId,
        final byte[] revealedProof
    ) throws Exception {
        final Function function = new Function(
            "withdraw",
            List.of(
                new Bytes32(Numeric.hexStringToByteArray(contractId)),
                new DynamicBytes(revealedProof)),
            List.of(new TypeReference<Bool>() {})
        );
        final String data = FunctionEncoder.encode(function);
        final RawTransactionManager manager = new RawTransactionManager(
            ethProvider, bobEthWallet);
        return manager.sendTransaction(
            BigInteger.valueOf(20_000_000_000L),
            BigInteger.valueOf(200_000),
            HTLC_ADDRESS,
            data,
            BigInteger.ZERO
        ).getTransactionHash();
    }

    private TransactionReceipt waitForEthereumReceipt(
        final String transactionHash
    ) throws Exception {
        return new PollingTransactionReceiptProcessor(
            ethProvider, 1000, 60).waitForTransactionReceipt(
                transactionHash);
    }

    private static byte[] sha256(
        final byte[] data
        ) throws NoSuchAlgorithmException {
            return MessageDigest.getInstance("SHA-256").digest(data);
        }
    }

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 (creates the ETH lock, claims XYM on Symbol)
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 (creates the XYM lock, claims ETH on Ethereum)
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);
        // Symbol accounts
        // Alice (creates the ETH lock, claims XYM on Symbol)
        final String aliceXymPrivateKey = System.getenv().getOrDefault(
            "ALICE_XYM_PRIVATE_KEY", "0".repeat(64));
        aliceXymKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(aliceXymPrivateKey));
        aliceXymAddress = facade.network.publicKeyToAddress(
            aliceXymKeyPair.getPublicKey());
        System.out.printf("Alice Symbol address: %s%n", aliceXymAddress);

        // Bob (creates the XYM lock, claims ETH on Ethereum)
        final String bobXymPrivateKey = System.getenv().getOrDefault(
            "BOB_XYM_PRIVATE_KEY", "1".repeat(64));
        bobXymKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(bobXymPrivateKey));
        bobXymAddress = facade.network.publicKeyToAddress(
            bobXymKeyPair.getPublicKey());
        System.out.printf("Bob Symbol address: %s%n", bobXymAddress);

        // Ethereum accounts
        ethProvider = Web3j.build(new HttpService(ethRpcUrl));

        final String aliceEthPrivateKey = System.getenv().getOrDefault(
            "ALICE_ETH_PRIVATE_KEY",
            "a73276699ba72dc7b5c9d08deaf8cd88eec33c866341b12030443"
                + "2b89586d45d");
        aliceEthWallet = Credentials.create(aliceEthPrivateKey);
        System.out.printf("Alice ETH address: %s%n",
            aliceEthWallet.getAddress());

        final String bobEthPrivateKey = System.getenv().getOrDefault(
            "BOB_ETH_PRIVATE_KEY",
            "8e85561005f27d926af79a7ce3e76e75108a09ff2ab78bf65b557"
                + "8d2e5d509bf");
        bobEthWallet = Credentials.create(bobEthPrivateKey);
        System.out.printf("Bob ETH address: %s%n",
            bobEthWallet.getAddress());

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

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

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

    proof = secrets.token_bytes(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'));
        System.out.println(
            "\n--- Alice: Generate proof and hashlock ---");

        final byte[] proof = new byte[32];
        RANDOM.nextBytes(proof);
        System.out.printf("Proof (hex): %s%n",
            HexFormat.of().formatHex(proof));

        final byte[] firstHash = sha256(proof);
        final byte[] secret = sha256(firstHash);
        System.out.printf("Secret (double SHA-256): %s%n",
            HexFormat.of().formatHex(secret));

スワップの開始者として、アリスはランダムな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);
        System.out.println(
            "\n--- Step 1. Alice: Lock ETH on Ethereum ---");

        final long timelock =
            System.currentTimeMillis() / 1000 + (72 * 60 * 60);
        System.out.printf("Ethereum timelock (Unix): %d%n", timelock);

        final String lockHash = sendNewContract(
            bobEthWallet.getAddress(),
            secret,
            BigInteger.valueOf(timelock));
        System.out.printf("Lock TX hash: %s%n", lockHash);

        final TransactionReceipt lockReceipt = waitForEthereumReceipt(
            lockHash);
        System.out.printf("Lock confirmed in block %s%n",
            lockReceipt.getBlockNumber());

        // Extract the contractId from the LogHTLCNew event
        final String contractId = lockReceipt.getLogs().get(0)
            .getTopics().get(1);
        System.out.printf("HTLC contract ID: %s%n", 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.create_transaction_from_descriptor(
        {
            'type': 'secret_lock_transaction_v1',
            'recipient_address': alice_xym_address,
            'secret': Hash256(hashlock),
            'mosaic': {
                'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
                'amount': 1_000_000  # 1 XYM
            },
            'duration': lock_duration,
            'hash_algorithm': 'hash_256'
        },
        bob_xym_key_pair.public_key,
        get_fee_multiplier(),
        2 * 60 * 60)

    # 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_confirmation(lock_hash, '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.createTransactionFromTypedDescriptor(
            new descriptors.SecretLockTransactionV1Descriptor(
                aliceXymAddress,
                new Hash256(hashlock),
                new descriptors.UnresolvedMosaicDescriptor(
                    generateMosaicAliasId('symbol.xym'),
                    new models.Amount(1_000_000n) // 1 XYM
                ),
                new models.BlockDuration(lockDuration),
                models.LockHashAlgorithm.HASH_256
            ),
            bobXymKeyPair.publicKey,
            await getFeeMultiplier(),
            2 * 60 * 60);

    // 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 waitForConfirmation(lockHash, 'Secret lock');
        System.out.println(
            "\n--- Step 2. Bob: Create secret lock on Symbol ---");

        // Bob queries the Ethereum contract to get the hashlock
        final String hashlock = getContractHashlock(contractId);
        System.out.printf("Hashlock from chain: %s%n", hashlock);

        final long lockDuration = 5760; // ~48h at 30s blocks
        System.out.printf("Lock duration: %d blocks%n", lockDuration);

        final Transaction secretLockTransaction =
            facade.createTransactionFromTypedDescriptor(
                new SecretLockTransactionV1Descriptor(
                    aliceXymAddress,
                    new CryptoTypes.Hash256(hashlock),
                    new UnresolvedMosaicDescriptor(
                        new UnresolvedMosaicId(
                            IdGenerator.generateMosaicAliasId(
                                "symbol.xym")),
                        new Amount(1_000_000)), // 1 XYM
                    new BlockDuration(lockDuration),
                    LockHashAlgorithm.HASH_256),
                bobXymKeyPair.getPublicKey(),
                getFeeMultiplier(),
                2 * 60 * 60);

        // Sign and announce
        final CryptoTypes.Signature lockSignature =
            facade.signTransaction(bobXymKeyPair, secretLockTransaction);
        final String lockPayload = SymbolTransactionFactory
            .attachSignature(secretLockTransaction, lockSignature);

        System.out.println("Built secret lock transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(secretLockTransaction.toJson()));

        final String symbolLockHash = facade.hashTransaction(
            secretLockTransaction).toString();
        System.out.printf("Secret lock transaction hash: %s%n",
            symbolLockHash);
        announceTransaction(lockPayload, "/transactions", "secret lock");
        waitForConfirmation(symbolLockHash, "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.create_transaction_from_descriptor(
        {
            'type': 'secret_proof_transaction_v1',
            'recipient_address': alice_xym_address,
            'secret': Hash256(hashlock),
            'hash_algorithm': 'hash_256',
            'proof': proof
        },
        alice_xym_key_pair.public_key,
        get_fee_multiplier(),
        2 * 60 * 60)

    # 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_confirmation(proof_hash, 'Secret proof')
    console.log('\n--- Step 3. Alice: Claim XYM on Symbol ---');

    const secretProofTransaction =
        facade.createTransactionFromTypedDescriptor(
            new descriptors.SecretProofTransactionV1Descriptor(
                aliceXymAddress,
                new Hash256(hashlock),
                models.LockHashAlgorithm.HASH_256,
                proof
            ),
            aliceXymKeyPair.publicKey,
            await getFeeMultiplier(),
            2 * 60 * 60);

    // 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 waitForConfirmation(proofHash, 'Secret proof');
        System.out.println(
            "\n--- Step 3. Alice: Claim XYM on Symbol ---");

        final Transaction secretProofTransaction =
            facade.createTransactionFromTypedDescriptor(
                new SecretProofTransactionV1Descriptor(
                    aliceXymAddress,
                    new CryptoTypes.Hash256(hashlock),
                    LockHashAlgorithm.HASH_256,
                    proof),
                aliceXymKeyPair.getPublicKey(),
                getFeeMultiplier(),
                2 * 60 * 60);

        // Sign and announce
        final CryptoTypes.Signature proofSignature =
            facade.signTransaction(
                aliceXymKeyPair, secretProofTransaction);
        final String proofPayload = SymbolTransactionFactory
            .attachSignature(secretProofTransaction, proofSignature);

        System.out.println("Built secret proof transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(secretProofTransaction.toJson()));

        final String proofHash = facade.hashTransaction(
            secretProofTransaction).toString();
        System.out.printf("Secret proof transaction hash: %s%n",
            proofHash);
        announceTransaction(proofPayload, "/transactions", "secret proof");
        waitForConfirmation(proofHash, "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);
        System.out.println(
            "\n--- Step 4. Bob: Withdraw ETH on Ethereum ---");

        // Bob waits for Alice to reveal the proof on Symbol.
        final byte[] revealedProof = waitForSecretProof(
            aliceXymAddress.toString(), hashlock);
        System.out.printf("Proof from chain: %s%n",
            HexFormat.of().formatHex(revealedProof));

        final String withdrawHash = sendWithdraw(
            contractId, revealedProof);
        System.out.printf("Withdraw TX hash: %s%n", withdrawHash);

        final TransactionReceipt withdrawReceipt = waitForEthereumReceipt(
            withdrawHash);
        System.out.printf("Withdraw confirmed in block %s%n",
            withdrawReceipt.getBlockNumber());

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

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 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 confirmation...
  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 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 confirmation...
  Transaction status: unknown
  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 を使用して、イーサリアムコントラクトからハッシュロックを取得します。
  • 行 49: ボブのSymbolシークレットロックが承認されます。これで、アリスはXYMを請求できるようになります。
  • 行 66: アリスはシークレット証明トランザクションに証明を含めます。アナウンスされると、Symbol上で誰でも見れる状態(パブリック)になります。
  • 行 77: アリスのシークレット証明が承認されます。アリスは1 XYMを受け取ります。
  • 行 83: ボブは、Symbol上で承認されたアリスのトランザクションから公開された証明を取得し、それを使用してイーサリアムで引き出しを行います。
  • 行 85: ボブのイーサリアムでの引き出しが承認されます。 ボブはアリスの0.01 ETHを受け取り、スワップが完了します。

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

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

結論⚓︎

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

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

次のステップ⚓︎

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