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

他のアカウントの代理でのトランザクション手数料の支払い⚓︎

通常、ブロックチェーンを利用するには、トランザクション手数料を支払うためにネットワークのネイティブ通貨を所有している必要があります。 初心者にとって、これは取引所を通じて資金を入手し、KYC 手続きを完了させる必要があることを意味します。

幸いなことに、アプリがユーザーの代わりにトランザクション手数料を負担することで、よりスムーズな体験を提供できます。

Ethereum では、この機能は当初 EIP-4337 などの標準規格を通じて導入されました。これにより、アカウント抽象化とスポンサー付きトランザクションが可能になりました。 さらに、EIP-7702 を含む新しい提案では、通常のアカウントがスマートコントラクトのような挙動を採用できるようにすることで、このアプローチを発展させています。

Symbol では、アグリゲートトランザクション を使用して、同様の挙動を標準機能として実装できます。 このチュートリアルでは、Symbol の アカウント が別のアカウントの代わりにトランザクション手数料を支払うことを可能にする 2 つの手法を紹介します。

簡潔な例

簡略化のため、このチュートリアルではトランザクションを構築するコードのみを示します。

そのため、示されているコードは直接実行可能ではありません。 このチュートリアルで構築されたトランザクションをアナウンスし、承認を確認する方法については、転送トランザクション のチュートリアルを参照してください。

問題の定義⚓︎

ユーザーが自分自身の Symbol アカウントを制御し、メッセージのペイロードを含む 転送トランザクション を送信することでメッセージを交換する、メッセージングアプリケーションを考えてみましょう。 アプリケーションはメッセージをリレーし、新しいメッセージを受信したときに通知をトリガーします。 中央の権限が通信を検閲したりブロックしたりすることはできません。

メッセージは、意図した受信者のみが読み取れるように暗号化することもできますが、暗号化はこのチュートリアルの範囲外です。

FeeSponsorshipProblemclusterTransferメッセージトランザクションAアカウント ABアカウント BA->B🖂

ユーザビリティを向上させるために、アプリケーション開発者は、各ユーザーの自身のアカウントからメッセージを送信しつつ、ユーザーの代わりにトランザクション手数料を支払うことを選択します。 これらの手数料のコストは、例えば法定通貨で支払われる月額サブスクリプションなどの外部課金メカニズムを通じて回収できます。

この利便性と引き換えに、トランザクション手数料の支払いに限定された中央集権的なポイントが導入されますが、ユーザーが自分自身で手数料を管理することを選択すれば、個別にこのポイントを排除できます。

オプション 1: 手数料前払い⚓︎

このアプローチでは、アプリケーション開発者が制御するアカウントが、ユーザーのアカウントに対して 前払い手数料 の転送を行います。

資金が悪用されないことを保証するために、手数料前払いの転送とメッセージの転送の両方が、単一の アグリゲートトランザクション 内に組み込まれます。 アグリゲートは、アプリケーションアカウントとユーザーの両方によって署名され、後者によってアナウンスされます。

トランザクション手数料は、すべての埋め込みトランザクションが実行された後に差し引かれます。これにより、事前供給された金額をメッセージ送信者がすべてのトランザクション手数料の支払いに充てることが可能になります。

事前供給額は、両方の 埋め込みトランザクション の手数料をカバーするのに十分である必要があり、この場合、埋め込みトランザクションの順序は関係ありません。

Option1clusterAggregateアグリゲートトランザクションclusterT1埋め込みメッセージトランザクションclusterT2埋め込み事前資金供給トランザクションA1アカウント AB1アカウント BA1->B1🖂C2アプリアカウントA2アカウント AC2->A2💲

セキュリティ上の理由から、アプリケーションはアプリケーションアカウントに属するいかなる 秘密鍵 も保持すべきではありません。 代わりに、 アグリゲートコンプリートトランザクション を構築してオフチェーン API を通じて開発者の 署名 を要求するか、 アグリゲートボンデッドトランザクション を構築してオンチェーンの手段のみで署名を要求することができます。

開発者の署名が得られれば、アプリケーションはユーザーの署名を付加してアグリゲートトランザクションをアナウンスできます。これは、ユーザーのアカウント残高がゼロであっても可能です。

def build_prefunded_message_transaction(recipient_address, message):
    # Build the embedded message transaction
    message_transaction = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        # Account sending the message
        'signer_public_key': user_key_pair.public_key,
        'recipient_address': recipient_address,
        'message': message
    })

    # Build the embedded prefund transaction
    prefund_transaction = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        # Account funding the transaction fee
        'signer_public_key': app_key_pair.public_key,
        # Account receiving the funds
        'recipient_address':
            facade.network.public_key_to_address(
                user_key_pair.public_key),
        'mosaics': [{
            'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
            'amount': 0 # To be filled once value is known
        }]
    })

    # Build the wrapper complete aggregate transaction
    transaction = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        # This is the account that will pay for the transaction
        'signer_public_key': user_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions': [message_transaction, prefund_transaction]
    })
    # Calculate total fee, reserving space for a cosignature
    transaction.fee = sc.Amount(fee_mult * (transaction.size + 104))
    # Update the prefund amount to match the total fee
    prefund_transaction.mosaics[0].amount = transaction.fee
    # Update the embedded transaction hashes
    transaction.transactions_hash = sc.Hash256(
        facade.hash_embedded_transactions(
            [message_transaction, prefund_transaction]).bytes)

    # Sign the aggregate transaction using the user's signature
    facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(user_key_pair, transaction))
    # Attach the app's cosignature
    transaction.cosignatures.append(
        facade.cosign_transaction(app_key_pair, transaction))
    # Obtain the payload
    json_payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(user_key_pair, transaction))

    return (transaction, json_payload)
function buildPrefundedMessageTransaction(recipientAddress, message) {
    // Build the embedded message transaction
    const messageTransaction = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        // Account sending the message
        signerPublicKey: userKeyPair.publicKey,
        recipientAddress,
        message
    });

    // Build the embedded prefund transaction
    const prefundTransaction = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        // Account funding the transaction fee
        signerPublicKey: appKeyPair.publicKey,
        // Account receiving the funds
        recipientAddress: facade.network.publicKeyToAddress(
            userKeyPair.publicKey
        ),
        mosaics: [{
            mosaicId: generateMosaicAliasId('symbol.xym'),
            amount: 0 // To be filled once value is known
        }]
    });

    // Build the wrapper complete aggregate transaction
    const transaction = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        // This is the account that will pay for the transaction
        signerPublicKey: userKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        transactions: [messageTransaction, prefundTransaction]
    });
    // Calculate total fee, reserving space for a cosignature
    transaction.fee = new models.Amount(
        feeMult * (transaction.size + 104)
    );
    // Update the prefund amount to match the total fee
    prefundTransaction.mosaics[0].amount = transaction.fee;
    // Update the embedded transaction hashes
    transaction.transactionsHash = new models.Hash256(
        facade.static.hashEmbeddedTransactions(
            [messageTransaction, prefundTransaction]
        ).bytes
    );

    // Sign the aggregate transaction using the user's signature
    facade.transactionFactory.static.attachSignature(
        transaction,
        facade.signTransaction(userKeyPair, transaction)
    );
    // Attach the app's cosignature
    transaction.cosignatures.push(
        facade.cosignTransaction(appKeyPair, transaction)
    );
    // Obtain the payload
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction,
        facade.signTransaction(userKeyPair, transaction)
    );

    return { transaction, jsonPayload };
}

メッセージトランザクション⚓︎

ユーザーから受信者へメッセージを送信する埋め込みトランザクションは、標準的な 転送トランザクション です。

    # Build the embedded message transaction
    message_transaction = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        # Account sending the message
        'signer_public_key': user_key_pair.public_key,
        'recipient_address': recipient_address,
        'message': message
    })
    // Build the embedded message transaction
    const messageTransaction = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        // Account sending the message
        signerPublicKey: userKeyPair.publicKey,
        recipientAddress,
        message
    });

事前資金供給トランザクション⚓︎

これも転送トランザクションですが、転送する金額がまだ決まっていないという特徴があります。 総手数料はアグリゲートトランザクションの最終的なサイズに依存し、この段階では計算できません。 そのため、金額は最初 0 に設定されます。

このトランザクションの送信者はアプリケーションアカウントで、受信者はユーザーアカウントです。

    # Build the embedded prefund transaction
    prefund_transaction = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        # Account funding the transaction fee
        'signer_public_key': app_key_pair.public_key,
        # Account receiving the funds
        'recipient_address':
            facade.network.public_key_to_address(
                user_key_pair.public_key),
        'mosaics': [{
            'mosaic_id': generate_mosaic_alias_id('symbol.xym'),
            'amount': 0 # To be filled once value is known
        }]
    })
    // Build the embedded prefund transaction
    const prefundTransaction = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        // Account funding the transaction fee
        signerPublicKey: appKeyPair.publicKey,
        // Account receiving the funds
        recipientAddress: facade.network.publicKeyToAddress(
            userKeyPair.publicKey
        ),
        mosaics: [{
            mosaicId: generateMosaicAliasId('symbol.xym'),
            amount: 0 // To be filled once value is known
        }]
    });

アグリゲートトランザクション⚓︎

アグリゲートコンプリートトランザクション は通常通り構築され、トランザクションサイズが判明した時点でその fee フィールドが更新されます。

その後、事前資金供給トランザクションの金額が、計算された手数料と一致するように設定されます。

最後に、 transactions_hash フィールドが埋め込みトランザクションの ハッシュ で更新されます。 このフィールドは通常、 を使用してアグリゲートが作成されるときに設定されますが、このケースでは事前資金供給トランザクションが修正された後に更新する必要があります。

注意

コードに示されているように、 transactions_hash フィールドを設定する際は、モデル固有の型である sc.Hash256 () または models.Hash256 () を使用し、汎用的な暗号化型である Hash256 は使用しないでください。

    # Build the wrapper complete aggregate transaction
    transaction = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        # This is the account that will pay for the transaction
        'signer_public_key': user_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions': [message_transaction, prefund_transaction]
    })
    # Calculate total fee, reserving space for a cosignature
    transaction.fee = sc.Amount(fee_mult * (transaction.size + 104))
    # Update the prefund amount to match the total fee
    prefund_transaction.mosaics[0].amount = transaction.fee
    # Update the embedded transaction hashes
    transaction.transactions_hash = sc.Hash256(
        facade.hash_embedded_transactions(
            [message_transaction, prefund_transaction]).bytes)
    // Build the wrapper complete aggregate transaction
    const transaction = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        // This is the account that will pay for the transaction
        signerPublicKey: userKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        transactions: [messageTransaction, prefundTransaction]
    });
    // Calculate total fee, reserving space for a cosignature
    transaction.fee = new models.Amount(
        feeMult * (transaction.size + 104)
    );
    // Update the prefund amount to match the total fee
    prefundTransaction.mosaics[0].amount = transaction.fee;
    // Update the embedded transaction hashes
    transaction.transactionsHash = new models.Hash256(
        facade.static.hashEmbeddedTransactions(
            [messageTransaction, prefundTransaction]
        ).bytes
    );

署名⚓︎

ユーザーアカウントは、アグリゲートトランザクションの署名者であるため、 を使用して自身の署名を追加します。 その後、前述のようにオンチェーンまたはオフチェーンのいずれかの方法で取得されたアプリケーションアカウントの 連署 を使用して追加されます。

    # Sign the aggregate transaction using the user's signature
    facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(user_key_pair, transaction))
    # Attach the app's cosignature
    transaction.cosignatures.append(
        facade.cosign_transaction(app_key_pair, transaction))
    # Obtain the payload
    json_payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(user_key_pair, transaction))
    // Sign the aggregate transaction using the user's signature
    facade.transactionFactory.static.attachSignature(
        transaction,
        facade.signTransaction(userKeyPair, transaction)
    );
    // Attach the app's cosignature
    transaction.cosignatures.push(
        facade.cosignTransaction(appKeyPair, transaction)
    );
    // Obtain the payload
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction,
        facade.signTransaction(userKeyPair, transaction)
    );

ペイロードが取得できれば、トランザクションをアナウンスし、承認を確認する準備が整います。

オプション 2: 手数料スポンサーシップ⚓︎

アグリゲートトランザクションでは、すべてのトランザクション手数料はアグリゲート署名者によってのみ支払われます。 したがって、提示された問題に対する直接的な解決策は、アプリケーションアカウントによって署名されたアグリゲートトランザクション内にメッセージトランザクションを埋め込むことです。

ただし、Symbol ではすべてのアグリゲート署名者が少なくとも 1 つの埋め込みトランザクションに参加している必要があります。 その結果、アプリケーションアカウントの署名を必要とする、追加の「フィラー (filler)」埋め込みトランザクションを含める必要があります。

このフィラートランザクションは副作用を持たない必要があり、アプリケーションアカウントから自分自身への空の転送が適した選択肢となります。

Option2clusterAggregateアグリゲートトランザクションclusterT1埋め込みメッセージトランザクションclusterT2埋め込みフィラートランザクションA1アカウント AB1アカウント BA1->B1🖂C2アプリアカウントA2アプリアカウントC2->A20

オプション 1 と比較して、このアプローチはセットアップがより簡単です。埋め込みトランザクションの構築後に総手数料を計算したり、トランザクションやそのハッシュを更新したりする必要がないためです。 一方で、アプリケーションアカウントがより積極的な役割を果たすことになります。これは、ユーザーが最終的に自身の資金や手数料を管理できるようにすることを目標としている場合には、逆効果になる可能性があります。

また、フィラートランザクションは事前資金供給の転送よりもサイズが小さいため、オプション 2 の方がわずかに安価です。

def build_sponsored_message_transaction(recipient_address, message):
    # Build the embedded message transaction
    message_transaction = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        # Account sending the message
        'signer_public_key': user_key_pair.public_key,
        'recipient_address': recipient_address,
        'message': message
    })

    # Build the embedded filler transaction
    filler_transaction = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        # The application account is both the sender and the recipient
        # and there is no `mosaics` field
        'signer_public_key': app_key_pair.public_key,
        'recipient_address':
            facade.network.public_key_to_address(
                app_key_pair.public_key)
    })

    # Build the wrapper complete aggregate transaction
    transaction = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        # This is the account that will pay for the transaction
        'signer_public_key': app_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash': facade.hash_embedded_transactions(
            [message_transaction, filler_transaction]),
        'transactions': [message_transaction, filler_transaction]
    })
    # Calculate total fee, reserving space for a cosignature
    transaction.fee = sc.Amount(fee_mult * (transaction.size + 104))

    # Sign the aggregate transaction using the app's signature
    facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(app_key_pair, transaction))
    # Attach the users's cosignature
    transaction.cosignatures.append(
        facade.cosign_transaction(user_key_pair, transaction))
    # Obtain the payload
    json_payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(app_key_pair, transaction))

    return (transaction, json_payload)
function buildSponsoredMessageTransaction(recipientAddress, message) {
    // Build the embedded message transaction
    const messageTransaction = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        // Account sending the message
        signerPublicKey: userKeyPair.publicKey,
        recipientAddress,
        message
    });

    // Build the embedded filler transaction
    const fillerTransaction = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        // The application account is both the sender and the recipient
        // and there is no `mosaics` field
        signerPublicKey: appKeyPair.publicKey,
        recipientAddress: facade.network.publicKeyToAddress(
            appKeyPair.publicKey
        )
    });

    // Build the wrapper complete aggregate transaction
    const transaction = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        // This is the account that will pay for the transaction
        signerPublicKey: appKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            [messageTransaction, fillerTransaction]),
        transactions: [messageTransaction, fillerTransaction]
    });
    // Calculate total fee, reserving space for a cosignature
    transaction.fee = new models.Amount(
        feeMult * (transaction.size + 104)
    );

    // Sign the aggregate transaction using the app's signature
    facade.transactionFactory.static.attachSignature(
        transaction,
        facade.signTransaction(appKeyPair, transaction)
    );
    // Attach the user's cosignature
    transaction.cosignatures.push(
        facade.cosignTransaction(userKeyPair, transaction)
    );
    // Obtain the payload
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction,
        facade.signTransaction(appKeyPair, transaction)
    );

    return { transaction, jsonPayload };
}

メッセージトランザクション⚓︎

ユーザーから受信者へメッセージを送信する埋め込みトランザクションは、標準的な転送トランザクションです。

    # Build the embedded message transaction
    message_transaction = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        # Account sending the message
        'signer_public_key': user_key_pair.public_key,
        'recipient_address': recipient_address,
        'message': message
    })
    // Build the embedded message transaction
    const messageTransaction = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        // Account sending the message
        signerPublicKey: userKeyPair.publicKey,
        recipientAddress,
        message
    });

フィラートランザクション⚓︎

これもアプリケーションアカウントから自分自身への転送トランザクションであり、資金は転送されません。 前述のように、その唯一の目的は、アプリケーションアカウントがアグリゲートトランザクションに署名し、手数料を支払うことを可能にすることです。

    # Build the embedded filler transaction
    filler_transaction = facade.transaction_factory.create_embedded({
        'type': 'transfer_transaction_v1',
        # The application account is both the sender and the recipient
        # and there is no `mosaics` field
        'signer_public_key': app_key_pair.public_key,
        'recipient_address':
            facade.network.public_key_to_address(
                app_key_pair.public_key)
    })
    // Build the embedded filler transaction
    const fillerTransaction = facade.transactionFactory.createEmbedded({
        type: 'transfer_transaction_v1',
        // The application account is both the sender and the recipient
        // and there is no `mosaics` field
        signerPublicKey: appKeyPair.publicKey,
        recipientAddress: facade.network.publicKeyToAddress(
            appKeyPair.publicKey
        )
    });

アグリゲートトランザクション⚓︎

アグリゲートコンプリートトランザクションは通常通り構築され、トランザクションサイズが判明した時点でその fee フィールドを更新します。 オプション 1 とは異なり、さらに修正を加える必要はありません。

    # Build the wrapper complete aggregate transaction
    transaction = facade.transaction_factory.create({
        'type': 'aggregate_complete_transaction_v3',
        # This is the account that will pay for the transaction
        'signer_public_key': app_key_pair.public_key,
        'deadline': timestamp.add_hours(2).timestamp,
        'transactions_hash': facade.hash_embedded_transactions(
            [message_transaction, filler_transaction]),
        'transactions': [message_transaction, filler_transaction]
    })
    # Calculate total fee, reserving space for a cosignature
    transaction.fee = sc.Amount(fee_mult * (transaction.size + 104))
    // Build the wrapper complete aggregate transaction
    const transaction = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        // This is the account that will pay for the transaction
        signerPublicKey: appKeyPair.publicKey,
        deadline: timestamp.addHours(2).timestamp,
        transactionsHash: facade.static.hashEmbeddedTransactions(
            [messageTransaction, fillerTransaction]),
        transactions: [messageTransaction, fillerTransaction]
    });
    // Calculate total fee, reserving space for a cosignature
    transaction.fee = new models.Amount(
        feeMult * (transaction.size + 104)
    );

署名⚓︎

アプリケーションアカウントは、アグリゲートトランザクションの署名者であるため、 を使用して自身の署名を追加します。 この署名は、前述のようにオンチェーンまたはオフチェーンの方法で取得できます。

その後、ユーザーアカウントの 連署 を使用して追加されます。

    # Sign the aggregate transaction using the app's signature
    facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(app_key_pair, transaction))
    # Attach the users's cosignature
    transaction.cosignatures.append(
        facade.cosign_transaction(user_key_pair, transaction))
    # Obtain the payload
    json_payload = facade.transaction_factory.attach_signature(
        transaction,
        facade.sign_transaction(app_key_pair, transaction))
    // Sign the aggregate transaction using the app's signature
    facade.transactionFactory.static.attachSignature(
        transaction,
        facade.signTransaction(appKeyPair, transaction)
    );
    // Attach the user's cosignature
    transaction.cosignatures.push(
        facade.cosignTransaction(userKeyPair, transaction)
    );
    // Obtain the payload
    const jsonPayload = facade.transactionFactory.static.attachSignature(
        transaction,
        facade.signTransaction(appKeyPair, transaction)
    );

ペイロードが取得できれば、トランザクションをアナウンスし、承認を確認する準備が整います。

結論⚓︎

このチュートリアルでは、 アグリゲートトランザクション を使用して、あるアカウントが別のアカウントの代わりにトランザクション手数料を支払う 2 つの方法を紹介しました。 どちらのアプローチも、ユーザーの所有権と自身のアカウントに対する制御を維持しつつ、アプリケーションがトランザクション手数料をスポンサーすることを可能にします。

手数料の支払いとトランザクションの承認を分離することで、アプリケーションはシステムの分散型の特性を損なうことなく、新規ユーザーに対してよりスムーズなオンボーディング体験を提供できます。