Skip to content

Commit 6dd03fa

Browse files
committed
feat(sdk-coin-tempo): add transaction serialization
TICKET: WIN-8479
1 parent 9263bc1 commit 6dd03fa

3 files changed

Lines changed: 108 additions & 8 deletions

File tree

modules/sdk-coin-tempo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@bitgo/sdk-core": "^36.25.0",
4545
"@bitgo/secp256k1": "^1.8.0",
4646
"@bitgo/statics": "^58.19.0",
47+
"@ethereumjs/common": "^2.6.5",
4748
"ethers": "^5.7.2"
4849
},
4950
"devDependencies": {

modules/sdk-coin-tempo/src/lib/transaction.ts

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { BaseTransaction, ParseTransactionError, TransactionType } from '@bitgo/sdk-core';
99
import { BaseCoin as CoinConfig } from '@bitgo/statics';
10+
import { ethers } from 'ethers';
1011
import { Address, Hex, Tip20Operation } from './types';
1112

1213
/**
@@ -54,8 +55,105 @@ export class Tip20Transaction extends BaseTransaction {
5455
}
5556

5657
async serialize(signature?: { r: Hex; s: Hex; yParity: number }): Promise<Hex> {
57-
// TODO: Implement EIP-7702 transaction serialization with ethers.js
58-
throw new ParseTransactionError('Transaction serialization not yet implemented');
58+
const sig = signature || this._signature;
59+
return this.serializeTransaction(sig);
60+
}
61+
62+
/**
63+
* Encode calls as RLP tuples for atomic batch execution
64+
* @returns Array of [to, value, data] tuples
65+
* @private
66+
*/
67+
private encodeCallsAsTuples(): any[] {
68+
return this.txRequest.calls.map((call) => [
69+
call.to,
70+
call.value ? ethers.utils.hexlify(call.value) : '0x',
71+
call.data,
72+
]);
73+
}
74+
75+
/**
76+
* Encode EIP-2930 access list as RLP tuples
77+
* @returns Array of [address, storageKeys[]] tuples
78+
* @private
79+
*/
80+
private encodeAccessList(): any[] {
81+
return (this.txRequest.accessList ?? []).map((item: any) => [item.address, item.storageKeys || []]);
82+
}
83+
84+
/**
85+
* Build base RLP data array per Tempo EIP-7702 specification
86+
* @param callsTuples Encoded calls
87+
* @param accessTuples Encoded access list
88+
* @returns RLP-ready array of transaction fields
89+
* @private
90+
*/
91+
private buildBaseRlpData(callsTuples: any[], accessTuples: any[]): any[] {
92+
return [
93+
ethers.utils.hexlify(this.txRequest.chainId),
94+
this.txRequest.maxPriorityFeePerGas ? ethers.utils.hexlify(this.txRequest.maxPriorityFeePerGas.toString()) : '0x',
95+
ethers.utils.hexlify(this.txRequest.maxFeePerGas.toString()),
96+
ethers.utils.hexlify(this.txRequest.gas.toString()),
97+
callsTuples,
98+
accessTuples,
99+
'0x', // nonceKey (reserved for 2D nonce system)
100+
ethers.utils.hexlify(this.txRequest.nonce),
101+
'0x', // validBefore (reserved for time bounds)
102+
'0x', // validAfter (reserved for time bounds)
103+
this.txRequest.feeToken || '0x',
104+
'0x', // feePayerSignature (reserved for sponsorship)
105+
[], // authorizationList (EIP-7702)
106+
];
107+
}
108+
109+
/**
110+
* Encode secp256k1 signature as 65-byte envelope
111+
* @param signature ECDSA signature components
112+
* @returns Hex string of concatenated r (32) + s (32) + v (1) bytes
113+
* @private
114+
*/
115+
private encodeSignature(signature: { r: Hex; s: Hex; yParity: number }): string {
116+
const v = signature.yParity + 27;
117+
const signatureBytes = ethers.utils.concat([
118+
ethers.utils.zeroPad(signature.r, 32),
119+
ethers.utils.zeroPad(signature.s, 32),
120+
ethers.utils.hexlify(v),
121+
]);
122+
return ethers.utils.hexlify(signatureBytes);
123+
}
124+
125+
/**
126+
* RLP encode and prepend transaction type byte
127+
* @param rlpData Transaction fields array
128+
* @returns Hex string with 0x76 prefix
129+
* @private
130+
*/
131+
private rlpEncodeWithTypePrefix(rlpData: any[]): Hex {
132+
try {
133+
const encoded = ethers.utils.RLP.encode(rlpData);
134+
return ('0x76' + encoded.slice(2)) as Hex;
135+
} catch (error) {
136+
throw new ParseTransactionError(`Failed to RLP encode transaction: ${error}`);
137+
}
138+
}
139+
140+
/**
141+
* Serialize Tempo AA transaction (type 0x76) per EIP-7702 specification
142+
* Format: 0x76 || RLP([chainId, fees, gas, calls, accessList, nonce fields, feeToken, sponsorship, authList, signature?])
143+
* @param signature Optional ECDSA signature (omit for unsigned transactions)
144+
* @returns RLP-encoded transaction hex string
145+
* @private
146+
*/
147+
private serializeTransaction(signature?: { r: Hex; s: Hex; yParity: number }): Hex {
148+
const callsTuples = this.encodeCallsAsTuples();
149+
const accessTuples = this.encodeAccessList();
150+
const rlpData = this.buildBaseRlpData(callsTuples, accessTuples);
151+
152+
if (signature) {
153+
rlpData.push(this.encodeSignature(signature));
154+
}
155+
156+
return this.rlpEncodeWithTypePrefix(rlpData);
59157
}
60158

61159
getOperations(): Tip20Operation[] {

modules/sdk-coin-tempo/src/tempo.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import {
88
UnsignedSweepTxMPCv2,
99
TransactionBuilder,
1010
} from '@bitgo/abstract-eth';
11+
import type * as EthLikeCommon from '@ethereumjs/common';
1112
import { BaseCoin, BitGoBase, MPCAlgorithm } from '@bitgo/sdk-core';
12-
import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
13+
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
14+
import { Tip20TransactionBuilder } from './lib';
1315

1416
export class Tempo extends AbstractEthLikeNewCoins {
1517
protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
@@ -105,12 +107,11 @@ export class Tempo extends AbstractEthLikeNewCoins {
105107

106108
/**
107109
* Get transaction builder for Tempo
108-
* TODO: Implement TransactionBuilder for Tempo
110+
* Returns a TIP-20 transaction builder for Tempo-specific operations
111+
* @param common - Optional common chain configuration
109112
* @protected
110113
*/
111-
protected getTransactionBuilder(): TransactionBuilder {
112-
// TODO: Create and return TransactionBuilder instance
113-
// Return undefined cast as TransactionBuilder to prevent downstream services from breaking
114-
return undefined as unknown as TransactionBuilder;
114+
protected getTransactionBuilder(common?: EthLikeCommon.default): TransactionBuilder {
115+
return new Tip20TransactionBuilder(coins.get(this.getBaseChain())) as unknown as TransactionBuilder;
115116
}
116117
}

0 commit comments

Comments
 (0)