Skip to content
Merged
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
61 changes: 53 additions & 8 deletions modules/sdk-coin-iota/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
IOTA_KEY_BYTES_LENGTH,
IOTA_SIGNATURE_LENGTH,
} from './constants';
import utils from './utils';

export abstract class Transaction extends BaseTransaction {
static EMPTY_PUBLIC_KEY = Buffer.alloc(IOTA_KEY_BYTES_LENGTH);
Expand All @@ -38,7 +39,9 @@ export abstract class Transaction extends BaseTransaction {
private _gasSponsor?: string;
private _sender: string;
private _signature?: Signature;
private _serializedSignature?: string;
private _gasSponsorSignature?: Signature;
private _serializedGasSponsorSignature?: string;
private _txDataBytes?: Uint8Array<ArrayBufferLike>;
private _isSimulateTx: boolean;

Expand All @@ -47,12 +50,6 @@ export abstract class Transaction extends BaseTransaction {
this._sender = '';
this._rebuildRequired = false;
this._isSimulateTx = true;
this._signature = {
publicKey: {
pub: Transaction.EMPTY_PUBLIC_KEY.toString('hex'),
},
signature: Transaction.EMPTY_SIGNATURE,
};
}

get gasBudget(): number | undefined {
Expand Down Expand Up @@ -137,12 +134,10 @@ export abstract class Transaction extends BaseTransaction {
}

addSignature(publicKey: PublicKey, signature: Buffer): void {
this._signatures = [...this._signatures, signature.toString('hex')];
this._signature = { publicKey, signature };
}

addGasSponsorSignature(publicKey: PublicKey, signature: Buffer): void {
this._signatures = [...this._signatures, signature.toString('hex')];
this._gasSponsorSignature = { publicKey, signature };
}

Expand All @@ -154,6 +149,26 @@ export abstract class Transaction extends BaseTransaction {
return this.gasBudget?.toString();
}

get serializedGasSponsorSignature(): string | undefined {
return this._serializedGasSponsorSignature;
}

get serializedSignature(): string | undefined {
return this._serializedSignature;
}

serializeSignatures(): void {
this._signatures = [];
if (this._signature) {
this._serializedSignature = this.serializeSignature(this._signature as Signature);
this._signatures.push(this._serializedSignature);
}
if (this._gasSponsorSignature) {
this._serializedGasSponsorSignature = this.serializeSignature(this._gasSponsorSignature as Signature);
this._signatures.push(this._serializedGasSponsorSignature);
}
}

async toBroadcastFormat(): Promise<string> {
const txDataBytes: Uint8Array<ArrayBufferLike> = await this.build();
return toBase64(txDataBytes);
Expand Down Expand Up @@ -291,6 +306,7 @@ export abstract class Transaction extends BaseTransaction {
this._txDataBytes = await this._iotaTransaction.build();
this._rebuildRequired = false;
}
this.serializeSignatures();
return this._txDataBytes;
}

Expand All @@ -306,6 +322,15 @@ export abstract class Transaction extends BaseTransaction {
this._iotaTransaction.setSender(this.sender);
}

private serializeSignature(signature: Signature): string {
const pubKey = Buffer.from(signature.publicKey.pub, 'hex');
const serialized_sig = new Uint8Array(1 + signature.signature.length + pubKey.length);
serialized_sig.set([0x00]); //Hardcoding the signature scheme flag since we only support EDDSA for iota
serialized_sig.set(signature.signature, 1);
serialized_sig.set(pubKey, 1 + signature.signature.length);
return toBase64(serialized_sig);
}

private validateTxData(): void {
this.validateTxDataImplementation();
if (!this.sender || this.sender === '') {
Expand All @@ -329,5 +354,25 @@ export abstract class Transaction extends BaseTransaction {
`Gas payment objects count (${this.gasPaymentObjects.length}) exceeds maximum allowed (${MAX_GAS_PAYMENT_OBJECTS})`
);
}

if (
this._signature &&
!(
utils.isValidPublicKey(this._signature.publicKey.pub) &&
utils.isValidSignature(toBase64(this._signature.signature))
)
) {
throw new InvalidTransactionError('Invalid sender signature');
}

if (
this._gasSponsorSignature &&
!(
utils.isValidPublicKey(this._gasSponsorSignature.publicKey.pub) &&
utils.isValidSignature(toBase64(this._gasSponsorSignature.signature))
)
) {
throw new InvalidTransactionError('Invalid gas sponsor signature');
}
}
}
27 changes: 27 additions & 0 deletions modules/sdk-coin-iota/test/resources/iota.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,30 @@ export const generateObjects = (count: number): TransactionObjectInput[] => {
digest: `digest${i}`,
}));
};

// Test signature data for signature serialization tests
export const testSignature = {
// 64-byte signature (hex string)
signature: Buffer.from(
'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2' +
'c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4',
'hex'
),
// Public key (already defined in sender)
publicKey: {
pub: sender.publicKey,
},
};

export const testGasSponsorSignature = {
// 64-byte signature (hex string)
signature: Buffer.from(
'd4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5' +
'f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7',
'hex'
),
// Public key (already defined in gasSponsor)
publicKey: {
pub: gasSponsor.publicKey,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -306,4 +306,157 @@ describe('Iota Transfer Builder', () => {
should(() => factory.from('invalidRawTransaction')).throwError();
});
});

describe('Transaction Signing', () => {
it('should build transaction with sender signature', async function () {
const txBuilder = factory.getTransferBuilder();
txBuilder.sender(testData.sender.address);
txBuilder.recipients(testData.recipients);
txBuilder.paymentObjects(testData.paymentObjects);
txBuilder.gasData(testData.gasData);

const tx = (await txBuilder.build()) as TransferTransaction;

// Add signature
tx.addSignature(testData.testSignature.publicKey, testData.testSignature.signature);

// Rebuild to trigger serialization
await tx.build();

should.exist(tx.serializedSignature);
should.equal(tx.serializedSignature!.length > 0, true);
});

it('should build transaction with gas sponsor signature', async function () {
const txBuilder = factory.getTransferBuilder();
txBuilder.sender(testData.sender.address);
txBuilder.recipients(testData.recipients);
txBuilder.paymentObjects(testData.paymentObjects);
txBuilder.gasData(testData.gasData);
txBuilder.gasSponsor(testData.gasSponsor.address);

const tx = (await txBuilder.build()) as TransferTransaction;

// Add gas sponsor signature
tx.addGasSponsorSignature(testData.testGasSponsorSignature.publicKey, testData.testGasSponsorSignature.signature);

// Rebuild to trigger serialization
await tx.build();

should.exist(tx.serializedGasSponsorSignature);
should.equal(tx.serializedGasSponsorSignature!.length > 0, true);
});

it('should build transaction with both sender and gas sponsor signatures', async function () {
const txBuilder = factory.getTransferBuilder();
txBuilder.sender(testData.sender.address);
txBuilder.recipients(testData.recipients);
txBuilder.paymentObjects(testData.paymentObjects);
txBuilder.gasData(testData.gasData);
txBuilder.gasSponsor(testData.gasSponsor.address);

const tx = (await txBuilder.build()) as TransferTransaction;

// Add both signatures
tx.addSignature(testData.testSignature.publicKey, testData.testSignature.signature);
tx.addGasSponsorSignature(testData.testGasSponsorSignature.publicKey, testData.testGasSponsorSignature.signature);

// Rebuild to trigger serialization
await tx.build();

should.exist(tx.serializedSignature);
should.exist(tx.serializedGasSponsorSignature);
tx.signature.length.should.equal(2);
});

it('should add signature through builder and serialize correctly', async function () {
const txBuilder = factory.getTransferBuilder();
txBuilder.sender(testData.sender.address);
txBuilder.recipients(testData.recipients);
txBuilder.paymentObjects(testData.paymentObjects);
txBuilder.gasData(testData.gasData);

// Add signature through builder
txBuilder.addSignature(testData.testSignature.publicKey, testData.testSignature.signature);

const tx = (await txBuilder.build()) as TransferTransaction;

should.exist(tx.serializedSignature);
should.equal(typeof tx.serializedSignature, 'string');
// Verify signature array is populated
tx.signature.length.should.equal(1);
});

it('should add gas sponsor signature through builder and serialize correctly', async function () {
const txBuilder = factory.getTransferBuilder();
txBuilder.sender(testData.sender.address);
txBuilder.recipients(testData.recipients);
txBuilder.paymentObjects(testData.paymentObjects);
txBuilder.gasData(testData.gasData);
txBuilder.gasSponsor(testData.gasSponsor.address);

// Add gas sponsor signature through builder
txBuilder.addGasSponsorSignature(
testData.testGasSponsorSignature.publicKey,
testData.testGasSponsorSignature.signature
);

const tx = (await txBuilder.build()) as TransferTransaction;

should.exist(tx.serializedGasSponsorSignature);
should.equal(typeof tx.serializedGasSponsorSignature, 'string');
// Verify signature array is populated
tx.signature.length.should.equal(1);
});

it('should serialize signatures in correct order', async function () {
const txBuilder = factory.getTransferBuilder();
txBuilder.sender(testData.sender.address);
txBuilder.recipients(testData.recipients);
txBuilder.paymentObjects(testData.paymentObjects);
txBuilder.gasData(testData.gasData);
txBuilder.gasSponsor(testData.gasSponsor.address);

// Add signatures through builder
txBuilder.addSignature(testData.testSignature.publicKey, testData.testSignature.signature);
txBuilder.addGasSponsorSignature(
testData.testGasSponsorSignature.publicKey,
testData.testGasSponsorSignature.signature
);

const tx = (await txBuilder.build()) as TransferTransaction;

// Verify signatures are in correct order: sender first, gas sponsor second
tx.signature.length.should.equal(2);
tx.signature[0].should.equal(tx.serializedSignature);
tx.signature[1].should.equal(tx.serializedGasSponsorSignature);
});

it('should fail to add invalid sender signature via builder', function () {
const txBuilder = factory.getTransferBuilder();
txBuilder.sender(testData.sender.address);
txBuilder.recipients(testData.recipients);
txBuilder.paymentObjects(testData.paymentObjects);
txBuilder.gasData(testData.gasData);

// Builder should validate and throw when adding invalid signature
should(() => txBuilder.addSignature({ pub: 'tooshort' }, testData.testSignature.signature)).throwError(
'Invalid transaction signature'
);
});

it('should fail to add invalid gas sponsor signature via builder', function () {
const txBuilder = factory.getTransferBuilder();
txBuilder.sender(testData.sender.address);
txBuilder.recipients(testData.recipients);
txBuilder.paymentObjects(testData.paymentObjects);
txBuilder.gasData(testData.gasData);
txBuilder.gasSponsor(testData.gasSponsor.address);

// Builder should validate and throw when adding invalid signature
should(() =>
txBuilder.addGasSponsorSignature(testData.testGasSponsorSignature.publicKey, Buffer.from('invalid'))
).throwError('Invalid transaction signature');
});
});
});
Loading