Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions modules/sdk-coin-sol/src/lib/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
PublicKey as BasePublicKey,
BaseTransaction,
Entry,
InvalidTransactionError,
Expand Down Expand Up @@ -265,6 +266,39 @@ export class Transaction extends BaseTransaction {
this._versionedTransactionData = data;
}

/**
* Add a signature produced by the fee payer of this transaction.
*
* The fee payer is account 0 of the transaction message, so its signature belongs in slot 0.
* Use this for signatures produced by the enterprise fee address key (HSM single-sig) of a
* sponsored transaction.
*
* @param {BasePublicKey} publicKey The public key of the fee payer; must be account 0 of the message
* @param {Buffer} signature The signature of the transaction payload
*/
addFeePayerSignature(publicKey: BasePublicKey, signature: Buffer): void {
if (this._versionedTransaction) {
const account0 = this._versionedTransaction.message.staticAccountKeys[0]?.toBase58();
if (publicKey.pub !== account0) {
throw new SigningError(`Fee payer signature must come from account 0 of the message, got: ${publicKey.pub}`);
}
this._versionedTransaction.addSignature(new PublicKey(publicKey.pub), signature);
return;
}

if (this._solTransaction) {
// For legacy transactions @solana/web3.js places the fee payer first in the message
const feePayer = this._solTransaction.feePayer?.toBase58();
if (publicKey.pub !== feePayer) {
throw new SigningError(`Fee payer signature must come from account 0 of the message, got: ${publicKey.pub}`);
}
this._solTransaction.addSignature(new PublicKey(publicKey.pub), signature);
return;
}

throw new SigningError('Fee payer signature requires a built transaction');
}

/** @inheritdoc */
canSign(): boolean {
return true;
Expand Down
26 changes: 26 additions & 0 deletions modules/sdk-coin-sol/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,32 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder {
*/
protected getAdditionalSignatures(): Signature[] {
return this._signatures;
* Add a signature produced by the fee payer of the transaction being built.
*
* The fee payer is account 0 of the transaction message, so its signature belongs in slot 0.
* Use this for signatures produced by the enterprise fee address key (HSM single-sig) of a
* sponsored transaction; the key must match account 0.
*
* @param {BasePublicKey} publicKey The public key of the fee payer; must be account 0 of the message
* @param {Buffer} signature The signature of the transaction payload
*/
addFeePayerSignature(publicKey: BasePublicKey, signature: Buffer): void {
if (publicKey.pub !== this.getFeePayerAccountKey()) {
throw new SigningError(`Fee payer signature must come from account 0 of the message, got: ${publicKey.pub}`);
}
this.addSignature(publicKey, signature);
}

/**
* The account 0 key of the message being built, i.e. the fee payer: the first static account
* key of a versioned transaction, or the fee payer of a legacy transaction.
*/
private getFeePayerAccountKey(): string | undefined {
const versionedData = this._transaction.getVersionedTransactionData();
if (versionedData) {
return versionedData.staticAccountKeys[0];
}
return this._feePayer ?? this._sender;
}

/**
Expand Down
30 changes: 30 additions & 0 deletions modules/sdk-coin-sol/test/unit/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,36 @@ describe('Sol Transaction', () => {
});
});

describe('addFeePayerSignature should', () => {
it('fill the signature slot of the fee payer (account 0) of a legacy transaction', () => {
const tx = new Transaction(coin);
tx.fromRawTransaction(testData.RAW_TX_UNSIGNED);

// accountWithSeed is the fee payer of RAW_TX_UNSIGNED; take its real signature from the signed fixture
const signed = SolTransaction.from(Buffer.from(testData.RAW_TX_SIGNED, 'base64'));
const feePayerSignature = signed.signatures[0].signature as Buffer;
should.exist(feePayerSignature);

tx.addFeePayerSignature({ pub: testData.accountWithSeed.publicKey }, feePayerSignature);
should.equal(tx.toBroadcastFormat(), testData.RAW_TX_SIGNED);
});

it('fail for a key that is not account 0', () => {
const tx = new Transaction(coin);
tx.fromRawTransaction(testData.RAW_TX_UNSIGNED);
should(() => tx.addFeePayerSignature({ pub: testData.authAccount.pub }, Buffer.alloc(64))).throwError(
/account 0 of the message/
);
});

it('fail on an unbuilt transaction', () => {
const tx = new Transaction(coin);
should(() => tx.addFeePayerSignature({ pub: testData.authAccount.pub }, Buffer.alloc(64))).throwError(
'Fee payer signature requires a built transaction'
);
});
});

describe('transaction parsing', function () {
it('fromRawTransaction and toBroadcastFormat', async function () {
const tx = new Transaction(coin);
Expand Down
Loading