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
2 changes: 2 additions & 0 deletions modules/sdk-coin-vet/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { KeyPair } from './keyPair';
export { Transaction } from './transaction/transaction';
export { AddressInitializationTransaction } from './transaction/addressInitializationTransaction';
export { FlushTokenTransaction } from './transaction/flushTokenTransaction';
export { FlushCoinsTransaction } from './transaction/flushCoinsTransaction';
export { TokenTransaction } from './transaction/tokenTransaction';
export { StakingTransaction } from './transaction/stakingTransaction';
export { StakeClauseTransaction } from './transaction/stakeClauseTransaction';
Expand All @@ -23,6 +24,7 @@ export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
export { TransferBuilder } from './transactionBuilder/transferBuilder';
export { AddressInitializationBuilder } from './transactionBuilder/addressInitializationBuilder';
export { FlushTokenTransactionBuilder } from './transactionBuilder/flushTokenTransactionBuilder';
export { FlushCoinsTransactionBuilder } from './transactionBuilder/flushCoinsTransactionBuilder';
export { StakingBuilder } from './transactionBuilder/stakingBuilder';
export { StakeClauseTxnBuilder } from './transactionBuilder/stakeClauseTxnBuilder';
export { DelegateTxnBuilder } from './transactionBuilder/delegateTxnBuilder';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { TransactionType, InvalidTransactionError } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { Transaction as VetTransaction, Secp256k1 } from '@vechain/sdk-core';

import { Transaction } from './transaction';
import { VetTransactionData } from '../iface';

export class FlushCoinsTransaction extends Transaction {
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._type = TransactionType.FlushCoins;
}

/** @inheritdoc */
buildClauses(): void {
this._clauses = [
{
to: this._contract,
value: '0x0',
data: this._transactionData,
},
];
}

/** @inheritdoc */
toJson(): VetTransactionData {
return {
id: this.id,
chainTag: this.chainTag,
blockRef: this.blockRef,
expiration: this.expiration,
gasPriceCoef: this.gasPriceCoef,
gas: this.gas,
dependsOn: this.dependsOn,
nonce: this.nonce,
data: this.transactionData,
value: '0',
sender: this.sender,
to: this.contract,
};
}

/** @inheritdoc */
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
try {
if (!signedTx || !signedTx.body) {
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
}

this.rawTransaction = signedTx;

const body = signedTx.body;
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
this.blockRef = body.blockRef || '0x0';
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
this.clauses = body.clauses || [];
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
this.dependsOn = body.dependsOn || null;
this.nonce = String(body.nonce);
this.contract = body.clauses[0]?.to || '0x0';
this.transactionData = body.clauses[0]?.data || '0x0';
this.type = TransactionType.FlushCoins;

try {
if (signedTx.origin) {
this.sender = signedTx.origin.toString().toLowerCase();
}
} catch {
// unsigned transaction has no origin
}

if (signedTx.signature) {
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
}
}
} catch (e) {
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import assert from 'assert';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { TransactionType, BuildTransactionError } from '@bitgo/sdk-core';
import { flushCoinsData, flushCoinsMethodId } from '@bitgo/abstract-eth';
import { TransactionClause } from '@vechain/sdk-core';

import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from '../transaction/transaction';
import { FlushCoinsTransaction } from '../transaction/flushCoinsTransaction';
import utils from '../utils';

export class FlushCoinsTransactionBuilder extends TransactionBuilder {
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._transaction = new FlushCoinsTransaction(_coinConfig);
}

initBuilder(tx: FlushCoinsTransaction): void {
this._transaction = tx;
}

get flushCoinsTransaction(): FlushCoinsTransaction {
return this._transaction as FlushCoinsTransaction;
}

protected get transactionType(): TransactionType {
return TransactionType.FlushCoins;
}

/**
* Validates the transaction clauses for a flush coins transaction.
* The clause must call the parameterless `flush()` method (selector 0x6b9f96ea)
* on a valid forwarder address with zero value.
*/
protected isValidTransactionClauses(clauses: TransactionClause[]): boolean {
try {
if (!clauses || !Array.isArray(clauses) || clauses.length === 0) {
return false;
}

const clause = clauses[0];

if (!clause.to || !utils.isValidAddress(clause.to)) {
return false;
}

if (clause.value !== 0) {
return false;
}

if (!clause.data.startsWith(flushCoinsMethodId)) {
return false;
}

return true;
} catch (e) {
return false;
}
}

/** @inheritdoc */
validateTransaction(transaction?: FlushCoinsTransaction): void {
if (!transaction) {
throw new BuildTransactionError('transaction not defined');
}
assert(transaction.contract, 'Contract address is required');
this.validateAddress({ address: transaction.contract });
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
this.transaction.type = this.transactionType;
this.flushCoinsTransaction.transactionData = flushCoinsData();
await this.flushCoinsTransaction.build();
return this.transaction;
}
}
10 changes: 10 additions & 0 deletions modules/sdk-coin-vet/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { TransactionBuilder } from './transactionBuilder/transactionBuilder';
import { TransferBuilder } from './transactionBuilder/transferBuilder';
import { AddressInitializationBuilder } from './transactionBuilder/addressInitializationBuilder';
import { FlushTokenTransactionBuilder } from './transactionBuilder/flushTokenTransactionBuilder';
import { FlushCoinsTransactionBuilder } from './transactionBuilder/flushCoinsTransactionBuilder';
import { ExitDelegationBuilder } from './transactionBuilder/exitDelegationBuilder';
import { BurnNftBuilder } from './transactionBuilder/burnNftBuilder';
import { ClaimRewardsBuilder } from './transactionBuilder/claimRewardsBuilder';
import { Transaction } from './transaction/transaction';
import utils from './utils';
import { AddressInitializationTransaction } from './transaction/addressInitializationTransaction';
import { FlushTokenTransaction } from './transaction/flushTokenTransaction';
import { FlushCoinsTransaction } from './transaction/flushCoinsTransaction';
import { ExitDelegationTransaction } from './transaction/exitDelegation';
import { BurnNftTransaction } from './transaction/burnNftTransaction';
import { ClaimRewardsTransaction } from './transaction/claimRewards';
Expand Down Expand Up @@ -59,6 +61,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
const flushTokenTx = new FlushTokenTransaction(this._coinConfig);
flushTokenTx.fromDeserializedSignedTransaction(signedTx);
return this.getFlushTokenTransactionBuilder(flushTokenTx);
case TransactionType.FlushCoins:
const flushCoinsTx = new FlushCoinsTransaction(this._coinConfig);
flushCoinsTx.fromDeserializedSignedTransaction(signedTx);
return this.getFlushCoinsTransactionBuilder(flushCoinsTx);
case TransactionType.SendToken:
const tokenTransferTx = new TokenTransaction(this._coinConfig);
tokenTransferTx.fromDeserializedSignedTransaction(signedTx);
Expand Down Expand Up @@ -132,6 +138,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.initializeBuilder(tx, new FlushTokenTransactionBuilder(this._coinConfig));
}

getFlushCoinsTransactionBuilder(tx?: FlushCoinsTransaction): FlushCoinsTransactionBuilder {
return this.initializeBuilder(tx, new FlushCoinsTransactionBuilder(this._coinConfig));
}

getTokenTransactionBuilder(tx?: Transaction): TokenTransactionBuilder {
return this.initializeBuilder(tx, new TokenTransactionBuilder(this._coinConfig));
}
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-coin-vet/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { BaseUtils, TransactionRecipient, TransactionType } from '@bitgo/sdk-cor
import {
v4CreateForwarderMethodId,
flushForwarderTokensMethodIdV4,
flushCoinsMethodId,
getRawDecoded,
getBufferedByteCode,
} from '@bitgo/abstract-eth';
Expand Down Expand Up @@ -96,6 +97,8 @@ export class Utils implements BaseUtils {
return TransactionType.AddressInitialization;
} else if (clauses[0].data.startsWith(flushForwarderTokensMethodIdV4)) {
return TransactionType.FlushTokens;
} else if (clauses[0].data.startsWith(flushCoinsMethodId)) {
return TransactionType.FlushCoins;
} else if (clauses[0].data.startsWith(TRANSFER_TOKEN_METHOD_ID)) {
return TransactionType.SendToken;
} else if (clauses[0].data.startsWith(STAKING_METHOD_ID)) {
Expand Down
16 changes: 16 additions & 0 deletions modules/sdk-coin-vet/test/resources/vet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ export const FLUSH_TOKEN_DATA = '0x3ef133670000000000000000000000000000000000000
export const FLUSH_TOKEN_SIGNABLE_PAYLOAD =
'f8542788014ead140e77bbc140f83df83b94043fde2fa0ece77c2461a9571f3b1b1c6faca12080a43ef133670000000000000000000000000000000000000000000000000000456e6572677981808252088082faf8c0';

export const FLUSH_COINS_TRANSACTION =
'0xf22788014ead140e77bbc140dcdb94043fde2fa0ece77c2461a9571f3b1b1c6faca12080846b9f96ea81808252088082faf8c0';

export const FLUSH_COINS_DATA = '0x6b9f96ea';

export const FLUSH_COINS_SIGNABLE_PAYLOAD =
'f22788014ead140e77bbc140dcdb94043fde2fa0ece77c2461a9571f3b1b1c6faca12080846b9f96ea81808252088082faf8c0';

export const INVALID_TRANSACTION =
'0xf8bc2788014ea060b5b5997e40e0df94e5f4eec44adf19c4cbeec88016345785d8a000080818082520880830bf84fc101b882418e212a40b29da685a7312829e8d1d3708b654f4ddb4388a7a80f5af3e5423b455451901d9b837fe18501e6ea5ec7d3d2711f00073d553aabe40e0260ec8a6f00da557d0a1af66b82b457324bc6fc86c7f0362e76c15b64432e66b6fa62fca38c7d208a604d1a7ec5356c95fec7bc6f332d16718a91a53e0e99e3a81ae3205ab400';

Expand All @@ -88,6 +96,14 @@ export const TRANSFER_CLAUSE = [
},
];

export const FLUSH_COINS_CLAUSE = [
{
to: '0x043fde2fa0ece77c2461a9571f3b1b1c6faca120',
value: 0,
data: '0x6b9f96ea',
},
];

export const addresses = {
validAddresses: [
'0x7ca00e3bc8a836026c2917c6c7c6d049e52099dd',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import should from 'should';
import { coins } from '@bitgo/statics';
import { TransactionType } from '@bitgo/sdk-core';
import { TransactionBuilderFactory, FlushCoinsTransaction } from '../../src';
import * as testData from '../resources/vet';

describe('Flush Coins Transaction', () => {
const factory = new TransactionBuilderFactory(coins.get('tvet'));

describe('Succeed', () => {
it('should build a flush coins transaction', async function () {
const transaction = new FlushCoinsTransaction(coins.get('tvet'));
const txBuilder = factory.getFlushCoinsTransactionBuilder(transaction);
txBuilder.gas(21000);
txBuilder.nonce('64248');
txBuilder.blockRef('0x014ead140e77bbc1');
txBuilder.expiration(64);
txBuilder.gasPriceCoef(128);
txBuilder.contract(testData.FORWARDER_ADDRESS);
const tx = (await txBuilder.build()) as FlushCoinsTransaction;
should.equal(tx.gas, 21000);
should.equal(tx.nonce, '64248');
should.equal(tx.expiration, 64);
should.equal(tx.type, TransactionType.FlushCoins);
should.equal(tx.blockRef, '0x014ead140e77bbc1');
should.equal(tx.clauses.length, 1);
should.equal(tx.clauses[0].to, testData.FORWARDER_ADDRESS);
should.equal(tx.clauses[0].data, testData.FLUSH_COINS_DATA);
should.equal(tx.clauses[0].value, '0x0');
const rawTx = tx.toBroadcastFormat();
should.equal(txBuilder.isValidRawTransaction(rawTx), true);
rawTx.should.equal(testData.FLUSH_COINS_TRANSACTION);
});

it('should validate signable payload', async function () {
const transaction = new FlushCoinsTransaction(coins.get('tvet'));
const txBuilder = factory.getFlushCoinsTransactionBuilder(transaction);
txBuilder.gas(21000);
txBuilder.nonce('64248');
txBuilder.blockRef('0x014ead140e77bbc1');
txBuilder.expiration(64);
txBuilder.gasPriceCoef(128);
txBuilder.contract(testData.FORWARDER_ADDRESS);
const tx = (await txBuilder.build()) as FlushCoinsTransaction;
should.equal(tx.signablePayload.toString('hex'), testData.FLUSH_COINS_SIGNABLE_PAYLOAD);
});

it('should build and validate toJson', async function () {
const transaction = new FlushCoinsTransaction(coins.get('tvet'));
const txBuilder = factory.getFlushCoinsTransactionBuilder(transaction);
txBuilder.gas(21000);
txBuilder.nonce('64248');
txBuilder.blockRef('0x014ead140e77bbc1');
txBuilder.expiration(64);
txBuilder.gasPriceCoef(128);
txBuilder.contract(testData.FORWARDER_ADDRESS);
const tx = (await txBuilder.build()) as FlushCoinsTransaction;

const toJson = tx.toJson();
should.equal(toJson.nonce, '64248');
should.equal(toJson.gas, 21000);
should.equal(toJson.gasPriceCoef, 128);
should.equal(toJson.expiration, 64);
should.equal(toJson.data, testData.FLUSH_COINS_DATA);
should.equal(toJson.to, testData.FORWARDER_ADDRESS);
should.equal(toJson.value, '0');
});

it('should produce a valid raw transaction matching expected bytes', async () => {
const transaction = new FlushCoinsTransaction(coins.get('tvet'));
const txBuilder = factory.getFlushCoinsTransactionBuilder(transaction);
txBuilder.gas(21000);
txBuilder.nonce('64248');
txBuilder.blockRef('0x014ead140e77bbc1');
txBuilder.expiration(64);
txBuilder.gasPriceCoef(128);
txBuilder.contract(testData.FORWARDER_ADDRESS);
const tx = (await txBuilder.build()) as FlushCoinsTransaction;
should.equal(tx.toBroadcastFormat(), testData.FLUSH_COINS_TRANSACTION);
should.equal(txBuilder.isValidRawTransaction(testData.FLUSH_COINS_TRANSACTION), true);
});
});

describe('Fail', () => {
it('should fail with invalid contract address', async function () {
const transaction = new FlushCoinsTransaction(coins.get('tvet'));
const txBuilder = factory.getFlushCoinsTransactionBuilder(transaction);
should(() => txBuilder.contract('randomString')).throwError('Invalid address randomString');
});

it('should fail to validate raw tx that is not flush coins', async function () {
const txBuilder = factory.getFlushCoinsTransactionBuilder();
should.equal(txBuilder.isValidRawTransaction(testData.FLUSH_TOKEN_TRANSACTION), false);
});
});
});
1 change: 1 addition & 0 deletions modules/sdk-coin-vet/test/unit/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ describe('Vechain util library', function () {

it('should get correct transaction type from clause', function () {
should.equal(TransactionType.Send, utils.getTransactionTypeFromClause(testData.TRANSFER_CLAUSE));
should.equal(TransactionType.FlushCoins, utils.getTransactionTypeFromClause(testData.FLUSH_COINS_CLAUSE));
});

it('is valid public key', function () {
Expand Down
Loading