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
1 change: 1 addition & 0 deletions modules/sdk-coin-tempo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@bitgo/sdk-core": "^36.25.0",
"@bitgo/secp256k1": "^1.8.0",
"@bitgo/statics": "^58.19.0",
"@ethereumjs/common": "^2.6.5",
"ethers": "^5.7.2"
},
"devDependencies": {
Expand Down
102 changes: 100 additions & 2 deletions modules/sdk-coin-tempo/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

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

/**
Expand Down Expand Up @@ -54,8 +55,105 @@ export class Tip20Transaction extends BaseTransaction {
}

async serialize(signature?: { r: Hex; s: Hex; yParity: number }): Promise<Hex> {
// TODO: Implement EIP-7702 transaction serialization with ethers.js
throw new ParseTransactionError('Transaction serialization not yet implemented');
const sig = signature || this._signature;
return this.serializeTransaction(sig);
}

/**
* Encode calls as RLP tuples for atomic batch execution
* @returns Array of [to, value, data] tuples
* @private
*/
private encodeCallsAsTuples(): any[] {
return this.txRequest.calls.map((call) => [
call.to,
call.value ? ethers.utils.hexlify(call.value) : '0x',
call.data,
]);
}

/**
* Encode EIP-2930 access list as RLP tuples
* @returns Array of [address, storageKeys[]] tuples
* @private
*/
private encodeAccessList(): any[] {
return (this.txRequest.accessList ?? []).map((item: any) => [item.address, item.storageKeys || []]);
}

/**
* Build base RLP data array per Tempo EIP-7702 specification
* @param callsTuples Encoded calls
* @param accessTuples Encoded access list
* @returns RLP-ready array of transaction fields
* @private
*/
private buildBaseRlpData(callsTuples: any[], accessTuples: any[]): any[] {
return [
ethers.utils.hexlify(this.txRequest.chainId),
this.txRequest.maxPriorityFeePerGas ? ethers.utils.hexlify(this.txRequest.maxPriorityFeePerGas.toString()) : '0x',
ethers.utils.hexlify(this.txRequest.maxFeePerGas.toString()),
ethers.utils.hexlify(this.txRequest.gas.toString()),
callsTuples,
accessTuples,
'0x', // nonceKey (reserved for 2D nonce system)
ethers.utils.hexlify(this.txRequest.nonce),
'0x', // validBefore (reserved for time bounds)
'0x', // validAfter (reserved for time bounds)
this.txRequest.feeToken || '0x',
'0x', // feePayerSignature (reserved for sponsorship)
[], // authorizationList (EIP-7702)
];
}

/**
* Encode secp256k1 signature as 65-byte envelope
* @param signature ECDSA signature components
* @returns Hex string of concatenated r (32) + s (32) + v (1) bytes
* @private
*/
private encodeSignature(signature: { r: Hex; s: Hex; yParity: number }): string {
const v = signature.yParity + 27;
const signatureBytes = ethers.utils.concat([
ethers.utils.zeroPad(signature.r, 32),
ethers.utils.zeroPad(signature.s, 32),
ethers.utils.hexlify(v),
]);
return ethers.utils.hexlify(signatureBytes);
}

/**
* RLP encode and prepend transaction type byte
* @param rlpData Transaction fields array
* @returns Hex string with 0x76 prefix
* @private
*/
private rlpEncodeWithTypePrefix(rlpData: any[]): Hex {
try {
const encoded = ethers.utils.RLP.encode(rlpData);
return ('0x76' + encoded.slice(2)) as Hex;
} catch (error) {
throw new ParseTransactionError(`Failed to RLP encode transaction: ${error}`);
}
}

/**
* Serialize Tempo AA transaction (type 0x76) per EIP-7702 specification
* Format: 0x76 || RLP([chainId, fees, gas, calls, accessList, nonce fields, feeToken, sponsorship, authList, signature?])
* @param signature Optional ECDSA signature (omit for unsigned transactions)
* @returns RLP-encoded transaction hex string
* @private
*/
private serializeTransaction(signature?: { r: Hex; s: Hex; yParity: number }): Hex {
const callsTuples = this.encodeCallsAsTuples();
const accessTuples = this.encodeAccessList();
const rlpData = this.buildBaseRlpData(callsTuples, accessTuples);

if (signature) {
rlpData.push(this.encodeSignature(signature));
}

return this.rlpEncodeWithTypePrefix(rlpData);
}

getOperations(): Tip20Operation[] {
Expand Down
13 changes: 7 additions & 6 deletions modules/sdk-coin-tempo/src/tempo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import {
UnsignedSweepTxMPCv2,
TransactionBuilder,
} from '@bitgo/abstract-eth';
import type * as EthLikeCommon from '@ethereumjs/common';
import { BaseCoin, BitGoBase, MPCAlgorithm } from '@bitgo/sdk-core';
import { BaseCoin as StaticsBaseCoin } from '@bitgo/statics';
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
import { Tip20TransactionBuilder } from './lib';

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

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