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
11 changes: 11 additions & 0 deletions modules/sdk-coin-starknet/src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { StarknetResourceBounds } from './iface';

// OZ EthAccountUpgradeable class hash (v0.17.0) — secp256k1 signature verification
export const OZ_ETH_ACCOUNT_CLASS_HASH = '0x3940bc18abf1df6bc540cabadb1cad9486c6803b95801e57b6153ae21abfe06';

Expand Down Expand Up @@ -28,3 +30,12 @@ export const TRANSACTION_VERSION_3 = 3n;
export const L1_GAS_NAME = 0x4c315f474153n; // "L1_GAS"
export const L2_GAS_NAME = 0x4c325f474153n; // "L2_GAS"
export const L1_DATA_GAS_NAME = 0x4c315f44415441n; // "L1_DATA" — NOT "L1_DATA_GAS"

/** Default v3 resource bounds (matches TransactionBuilder defaults). */
export function defaultResourceBounds(): StarknetResourceBounds {
return {
l2_gas: { max_amount: '0x1c9c380', max_price_per_unit: '0x174876e800' },
l1_gas: { max_amount: '0x0', max_price_per_unit: '0x5af3107a4000' },
l1_data_gas: { max_amount: '0x3e8', max_price_per_unit: '0x2540be400' },
};
}
52 changes: 47 additions & 5 deletions modules/sdk-coin-starknet/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,29 @@ import {
InvalidTransactionError,
} from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { StarknetTransactionData, StarknetTransactionType, StarknetTransactionExplanation, TxData } from './iface';
import utils, { parseTransferCall } from './utils';
import { defaultResourceBounds } from './constants';
import {
StarknetResourceBounds,
StarknetTransactionData,
StarknetTransactionType,
StarknetTransactionExplanation,
TxData,
} from './iface';
import utils, { compileExecuteCalldata, parseTransferCall } from './utils';

function resolveCompiledCalldata(data: StarknetTransactionData): string[] {
if (data.compiledCalldata && data.compiledCalldata.length > 0) {
return data.compiledCalldata;
}
if (data.calls.length > 0) {
return compileExecuteCalldata(data.calls);
}
throw new InvalidTransactionError('Missing calldata: no compiledCalldata or calls');
}

function resolveResourceBounds(data: StarknetTransactionData): StarknetResourceBounds {
return data.resourceBounds ?? defaultResourceBounds();
}

export class Transaction extends BaseTransaction {
protected _starknetTransactionData!: StarknetTransactionData;
Expand Down Expand Up @@ -116,14 +137,35 @@ export class Transaction extends BaseTransaction {
};
}

/** @inheritdoc */
/** Hex-encoded internal JSON — used by WP for round-trip via fromRawTransaction. */
toInternalHex(): string {
const data = this._starknetTransactionData;
if (!data) {
throw new InvalidTransactionError('Empty transaction');
}
return Buffer.from(JSON.stringify(data), 'utf-8').toString('hex');
}

/** @inheritdoc — returns Starknet RPC-ready JSON string for starknet_addInvokeTransaction. */
toBroadcastFormat(): string {
const data = this._starknetTransactionData;
if (!data) {
throw new InvalidTransactionError('Empty transaction');
}
const json = JSON.stringify(data);
return Buffer.from(json, 'utf-8').toString('hex');
return JSON.stringify({
type: 'INVOKE',
version: '0x3',
sender_address: data.senderAddress,
calldata: resolveCompiledCalldata(data),
signature: data.signature || [],
nonce: data.nonce,
resource_bounds: resolveResourceBounds(data),
tip: data.tip || '0x0',
paymaster_data: [],
account_deployment_data: [],
nonce_data_availability_mode: 'L1',
fee_data_availability_mode: 'L1',
});
}

/** @inheritdoc */
Expand Down
9 changes: 1 addition & 8 deletions modules/sdk-coin-starknet/src/lib/transactionBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,10 @@ import {
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import BigNumber from 'bignumber.js';
import { StarknetTransactionData, StarknetTransactionType, StarknetCall, StarknetResourceBounds } from './iface';
import { defaultResourceBounds } from './constants';
import { Transaction } from './transaction';
import utils from './utils';

function defaultResourceBounds(): StarknetResourceBounds {
return {
l2_gas: { max_amount: '0x1c9c380', max_price_per_unit: '0x174876e800' },
l1_gas: { max_amount: '0x0', max_price_per_unit: '0x5af3107a4000' },
l1_data_gas: { max_amount: '0x3e8', max_price_per_unit: '0x2540be400' },
};
}

export abstract class TransactionBuilder extends BaseTransactionBuilder {
protected _transaction: Transaction;
protected _sender?: string;
Expand Down
33 changes: 30 additions & 3 deletions modules/sdk-coin-starknet/test/unit/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import should from 'should';
import { coins } from '@bitgo/statics';
import { Transaction } from '../../src/lib/transaction';
import { rawTx, Accounts } from '../resources/starknet';
import { Accounts, rawTx } from '../resources/starknet';

describe('Starknet Transaction', () => {
describe('Parse unsigned transaction', () => {
Expand Down Expand Up @@ -42,14 +42,41 @@ describe('Starknet Transaction', () => {
});

describe('toBroadcastFormat', () => {
it('should produce non-empty broadcast format for unsigned tx', async () => {
it('should return Starknet RPC-ready JSON string', async () => {
const coinConfig = coins.get('starknet');
const tx = new Transaction(coinConfig);
await tx.fromRawTransaction(rawTx.transfer.unsigned);

const payload = tx.toBroadcastFormat();
should.exist(payload);
payload.length.should.be.greaterThan(0);
const parsed = JSON.parse(payload);
parsed.type.should.equal('INVOKE');
parsed.version.should.equal('0x3');
parsed.should.have.property('sender_address');
parsed.sender_address.should.equal(Accounts.account1.address);
parsed.calldata.should.be.Array().and.not.empty();
parsed.calldata[0].should.equal('0x1');
parsed.should.have.property('nonce');
parsed.resource_bounds.should.have.properties(['l2_gas', 'l1_gas', 'l1_data_gas']);
parsed.resource_bounds.l2_gas.should.have.properties(['max_amount', 'max_price_per_unit']);
parsed.nonce_data_availability_mode.should.equal('L1');
parsed.fee_data_availability_mode.should.equal('L1');
});
});

describe('toInternalHex', () => {
it('should produce hex-encoded internal JSON', async () => {
const coinConfig = coins.get('starknet');
const tx = new Transaction(coinConfig);
await tx.fromRawTransaction(rawTx.transfer.unsigned);

const hex = tx.toInternalHex();
should.exist(hex);
const json = JSON.parse(Buffer.from(hex, 'hex').toString('utf-8'));
json.should.have.property('senderAddress');
json.should.have.property('calls');
json.should.have.property('chainId');
json.should.have.property('transactionType');
});
});
});
33 changes: 29 additions & 4 deletions modules/sdk-coin-starknet/test/unit/transferBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ describe('Starknet TransferBuilder', () => {
tx1.signableHex.should.not.equal(tx2.signableHex);
});

it('should round-trip through toBroadcastFormat and fromRawTransaction', async () => {
it('should round-trip through toInternalHex and fromRawTransaction', async () => {
const factory = new TransactionBuilderFactory(coinConfig);
const builder = factory.getTransferBuilder();

Expand All @@ -173,15 +173,40 @@ describe('Starknet TransferBuilder', () => {
.amount('1000000000000000000');

const tx = (await builder.build()) as Transaction;
const broadcastHex = tx.toBroadcastFormat();
const internalHex = tx.toInternalHex();

const factory2 = new TransactionBuilderFactory(coinConfig);
const builder2 = await factory2.from(broadcastHex);
const tx2 = (await builder2.build()) as Transaction as Transaction;
const builder2 = await factory2.from(internalHex);
const tx2 = (await builder2.build()) as Transaction;

tx2.signableHex.should.equal(tx.signableHex);
tx2.id.should.equal(tx.id);
});

it('toBroadcastFormat should return Starknet RPC-ready JSON', async () => {
const factory = new TransactionBuilderFactory(coinConfig);
const builder = factory.getTransferBuilder();

builder
.sender(Accounts.account1.address)
.nonce('0x0')
.chainId(SandboxTransferData.chainId)
.receiverId(Accounts.account2.address)
.amount('1000000000000000000');

const tx = (await builder.build()) as Transaction;
const broadcast = tx.toBroadcastFormat();
const parsed = JSON.parse(broadcast);

parsed.type.should.equal('INVOKE');
parsed.version.should.equal('0x3');
parsed.sender_address.should.equal(Accounts.account1.address);
parsed.calldata.should.be.Array();
parsed.nonce.should.equal('0x0');
parsed.resource_bounds.should.have.property('l2_gas');
parsed.nonce_data_availability_mode.should.equal('L1');
parsed.fee_data_availability_mode.should.equal('L1');
});
});

describe('Validation', () => {
Expand Down
Loading