Skip to content

Commit

Permalink
Merge pull request #5451 from BitGo/WIN-4297
Browse files Browse the repository at this point in the history
feat: implement base transaction and transfer builders
  • Loading branch information
hitansh-madan authored Jan 30, 2025
2 parents 64a259e + b342edd commit fa245c4
Show file tree
Hide file tree
Showing 24 changed files with 2,719 additions and 57 deletions.
3 changes: 2 additions & 1 deletion modules/sdk-coin-tao/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,16 @@
"@bitgo/abstract-substrate": "^1.1.2",
"@bitgo/sdk-core": "^28.22.0",
"@bitgo/statics": "^50.22.0",
"bignumber.js": "^9.1.2",
"@polkadot/keyring": "13.3.1",
"@polkadot/types": "14.1.1",
"@polkadot/util": "13.3.1",
"@polkadot/util-crypto": "13.3.1",
"@substrate/txwrapper-core": "7.5.2",
"@substrate/txwrapper-polkadot": "7.5.2",
"bignumber.js": "^9.1.2",
"bs58": "^4.0.1",
"hi-base32": "^0.5.1",
"joi": "^17.4.0",
"lodash": "^4.17.15",
"tweetnacl": "^1.0.3"
},
Expand Down
15 changes: 15 additions & 0 deletions modules/sdk-coin-tao/src/lib/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BuildTransactionError } from '@bitgo/sdk-core';

export class AddressValidationError extends BuildTransactionError {
constructor(malformedAddress: string) {
super(`The address '${malformedAddress}' is not a well-formed dot address`);
this.name = AddressValidationError.name;
}
}

export class InvalidFeeError extends BuildTransactionError {
constructor(type?: string, expectedType?: string) {
super(`The specified type: "${type}" is not valid. Please provide the type: "${expectedType}"`);
this.name = InvalidFeeError.name;
}
}
100 changes: 100 additions & 0 deletions modules/sdk-coin-tao/src/lib/iface_utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
AccountId,
AddProxyArgs,
AddProxyBatchCallArgs,
StakeBatchCallPayee,
StakeBatchCallPayeeAccount,
StakeBatchCallPayeeController,
StakeBatchCallPayeeStaked,
StakeBatchCallPayeeStash,
ProxyArgs,
} from './iface';

/**
* Returns true if value is of type AccountId, false otherwise.
*
* @param value The object to test.
*
* @return true if value is of type AccountId, false otherwise.
*/
export function isAccountId(value: string | AccountId): value is AccountId {
return value.hasOwnProperty('id');
}

/**
* Extracts the proxy address being added from an add proxy batch call or an add proxy call.
* @param call A batched add proxy call or an add proxy call from which to extract the proxy
* address.
*
* @return the proxy address being added from an add proxy batch call or an add proxy call.
*/
export function getDelegateAddress(call: AddProxyBatchCallArgs | AddProxyArgs): string {
if (isAccountId(call.delegate)) {
return call.delegate.id;
} else {
return call.delegate;
}
}

/**
* Returns true if value is of type StakeBatchCallPayeeStaked, false otherwise.
*
* @param value The object to test.
*
* @return true if value is of type StakeBatchCallPayeeStaked, false otherwise.
*/
export function isStakeBatchCallPayeeStaked(value: StakeBatchCallPayee): value is StakeBatchCallPayeeStaked {
return (value as StakeBatchCallPayeeStaked).hasOwnProperty('staked');
}

/**
* Returns true if value is of type StakeBatchCallPayeeStash, false otherwise.
*
* @param value The object to test.
*
* @return true if value is of type StakeBatchCallPayeeStash, false otherwise.
*/
export function isStakeBatchCallPayeeStash(value: StakeBatchCallPayee): value is StakeBatchCallPayeeStash {
return (value as StakeBatchCallPayeeStash).hasOwnProperty('stash');
}

/**
* Returns true if value is of type StakeBatchCallPayeeController, false otherwise.
*
* @param value The object to test.
*
* @return true if value is of type StakeBatchCallPayeeController, false otherwise.
*/
export function isStakeBatchCallPayeeController(value: StakeBatchCallPayee): value is StakeBatchCallPayeeController {
return (value as StakeBatchCallPayeeController).hasOwnProperty('controller');
}

/**
* Returns true if value is of type StakeBatchCallPayeeAccount, false otherwise.
*
* @param value The object to test.
*
* @return true if value is of type StakeBatchCallPayeeAccount, false otherwise.
*/
export function isStakeBatchCallPayeeAccount(value: StakeBatchCallPayee): value is StakeBatchCallPayeeAccount {
return (
(value as StakeBatchCallPayeeAccount).account !== undefined &&
(value as StakeBatchCallPayeeAccount).account !== null
);
}

/**
* Extracts the proxy address being added from ProxyArgs.
* @param args the ProxyArgs object from which to extract the proxy address.
*
* @return the proxy address being added.
*/
export function getAddress(args: ProxyArgs): string {
if (isAccountId(args.real)) {
return args.real.id;
} else {
return args.real;
}
}
2 changes: 2 additions & 0 deletions modules/sdk-coin-tao/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ export { Transaction } from './transaction';
export { TransactionBuilder } from './transactionBuilder';
export { TransferBuilder } from './transferBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { SingletonRegistry } from './singletonRegistry';
export { NativeTransferBuilder } from './nativeTransferBuilder';
export { Interface, Utils };
256 changes: 256 additions & 0 deletions modules/sdk-coin-tao/src/lib/nativeTransferBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { BaseAddress, DotAssetTypes, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { methods } from '@substrate/txwrapper-polkadot';
import { DecodedSignedTx, DecodedSigningPayload, UnsignedTransaction } from '@substrate/txwrapper-core';
import BigNumber from 'bignumber.js';
import { MethodNames, ProxyArgs, ProxyType, TransferAllArgs, TransferArgs } from './iface';
import { getAddress } from './iface_utils';
import { SingletonRegistry } from './singletonRegistry';
import { Transaction } from './transaction';
import { TransactionBuilder } from './transactionBuilder';
import { ProxyTransactionSchema, TransferAllTransactionSchema, TransferTransactionSchema } from './txnSchema';
import utils from './utils';

export abstract class NativeTransferBuilder extends TransactionBuilder {
protected _sweepFreeBalance = false;
protected _keepAddressAlive = true;
protected _amount: string;
protected _to: string;
protected _owner: string;
protected _forceProxyType: ProxyType;

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}

/**
*
* Dispatch the given call from an account that the sender is authorised for through add_proxy.
*
* @returns {UnsignedTransaction} an unsigned Dot transaction
*
* @see https://polkadot.js.org/docs/substrate/extrinsics/#proxy
*/
protected buildTransaction(): UnsignedTransaction {
const baseTxInfo = this.createBaseTxInfo();
let transferTx;
if (this._sweepFreeBalance) {
transferTx = methods.balances.transferAll(
{
dest: { id: this._to },
keepAlive: this._keepAddressAlive,
},
baseTxInfo.baseTxInfo,
baseTxInfo.options
);
} else {
transferTx = methods.balances.transferKeepAlive(
{
value: this._amount,
dest: { id: this._to },
},
baseTxInfo.baseTxInfo,
baseTxInfo.options
);
}

if (!this._owner) {
return transferTx;
}
return methods.proxy.proxy(
{
real: this._owner,
forceProxyType: this._forceProxyType,
call: transferTx.method,
},
baseTxInfo.baseTxInfo,
baseTxInfo.options
);
}

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

/**
*
* Set this to be a sweep transaction, using TransferAll with keepAlive set to true by default.
* If keepAlive is false, the entire address will be swept (including the 1 DOT minimum).
*
* @param {boolean} keepAlive - keep the address alive after this sweep
* @returns {TransferBuilder} This transfer builder.
*
* @see https://github.com/paritytech/txwrapper-core/blob/main/docs/modules/txwrapper_substrate_src.methods.balances.md#transferall
*/
sweep(keepAlive?: boolean): this {
this._sweepFreeBalance = true;
if (keepAlive !== undefined) {
this._keepAddressAlive = keepAlive;
}
return this;
}

/**
*
* The amount for transfer transaction.
*
* @param {string} amount
* @returns {TransferBuilder} This transfer builder.
*
* @see https://wiki.polkadot.network/docs/build-protocol-info
*/
amount(amount: string): this {
this.validateValue(new BigNumber(amount));
this._amount = amount;
return this;
}

/**
*
* The destination address for transfer transaction.
*
* @param {string} dest
* @returns {TransferBuilder} This transfer builder.
*
* @see https://wiki.polkadot.network/docs/build-protocol-info
*/
to({ address }: BaseAddress): this {
this.validateAddress({ address });
this._to = address;
return this;
}

/**
*
* The real address of the original tx
*
* @param {BaseAddress} real
* @returns {TransferBuilder} This builder.
*
* @see https://wiki.polkadot.network/docs/learn-proxies#why-use-a-proxy
*/
owner(owner: BaseAddress): this {
this.validateAddress({ address: owner.address });
this._owner = owner.address;
return this;
}

/**
*
* The proxy type to execute
*
* @param {proxyType} forceProxyType
* @returns {TransferBuilder} This builder.
*
* @see https://wiki.polkadot.network/docs/learn-proxies#proxy-types
*/
forceProxyType(forceProxyType: ProxyType): this {
this._forceProxyType = forceProxyType;
return this;
}

/** @inheritdoc */
validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx, rawTransaction: string): void {
if (decodedTxn.method?.name === MethodNames.TransferKeepAlive) {
const txMethod = decodedTxn.method.args as unknown as TransferArgs;
const amount = `${txMethod.value}`;
const to = txMethod.dest.id;
const validationResult = TransferTransactionSchema.validate({ amount, to });
if (validationResult.error) {
throw new InvalidTransactionError(`Transfer Transaction validation failed: ${validationResult.error.message}`);
}
} else if (decodedTxn.method?.name === MethodNames.Proxy) {
const txMethod = decodedTxn.method.args as unknown as ProxyArgs;
const real = getAddress(txMethod);
const forceProxyType = txMethod.forceProxyType;
const decodedCall = utils.decodeCallMethod(rawTransaction, {
registry: SingletonRegistry.getInstance(this._material),
metadataRpc: this._material.metadata,
});
const amount = `${decodedCall.value}`;
const to = decodedCall.dest.id;
const validationResult = ProxyTransactionSchema.validate({ real, forceProxyType, amount, to });
if (validationResult.error) {
throw new InvalidTransactionError(`Proxy Transaction validation failed: ${validationResult.error.message}`);
}
}
}

/** @inheritdoc */
protected fromImplementation(rawTransaction: string): Transaction {
const tx = super.fromImplementation(rawTransaction);
if (this._method?.name === MethodNames.TransferKeepAlive) {
const txMethod = this._method.args as TransferArgs;
this.amount(txMethod.value);
this.to({
address: utils.decodeDotAddress(
txMethod.dest.id,
utils.getAddressFormat(this._coinConfig.name as DotAssetTypes)
),
});
} else if (this._method?.name === MethodNames.TransferAll) {
this._sweepFreeBalance = true;
const txMethod = this._method.args as TransferAllArgs;
this.sweep(txMethod.keepAlive);
this.to({
address: utils.decodeDotAddress(
txMethod.dest.id,
utils.getAddressFormat(this._coinConfig.name as DotAssetTypes)
),
});
} else if (this._method?.name === MethodNames.Proxy) {
const txMethod = this._method.args as ProxyArgs;
this.owner({
address: utils.decodeDotAddress(
getAddress(txMethod),
utils.getAddressFormat(this._coinConfig.name as DotAssetTypes)
),
});
this.forceProxyType(txMethod.forceProxyType);
const decodedCall = utils.decodeCallMethod(rawTransaction, {
registry: SingletonRegistry.getInstance(this._material),
metadataRpc: this._material.metadata,
});
if (!decodedCall.value || !decodedCall.dest) {
throw new InvalidTransactionError(
`Invalid Proxy Transaction Method: ${this._method?.name}. Expected transferKeepAlive`
);
}
this.amount(`${decodedCall.value}`);
this.to({
address: utils.decodeDotAddress(
decodedCall.dest.id,
utils.getAddressFormat(this._coinConfig.name as DotAssetTypes)
),
});
} else {
throw new InvalidTransactionError(
`Invalid Transaction Type: ${this._method?.name}. Expected a transferKeepAlive or a proxy transferKeepAlive transaction`
);
}
return tx;
}

/** @inheritdoc */
validateTransaction(_: Transaction): void {
super.validateTransaction(_);
this.validateFields(this._to, this._amount, this._owner, this._forceProxyType);
}

private validateFields(to: string, amount: string, real?: string, forceProxyType?: string): void {
let validationResult;
if (forceProxyType) {
validationResult = ProxyTransactionSchema.validate({ to, amount, real, forceProxyType });
} else if (this._sweepFreeBalance) {
validationResult = TransferAllTransactionSchema.validate({ to });
} else {
validationResult = TransferTransactionSchema.validate({ amount, to });
}

if (validationResult.error) {
throw new InvalidTransactionError(
`Proxy/TransferAll/TransferKeepAlive Transaction validation failed: ${validationResult.error.message}`
);
}
}
}
Loading

0 comments on commit fa245c4

Please sign in to comment.