Skip to content

Commit b342edd

Browse files
committed
feat: implement base transaction and transfer builders
TICKET: WIN-4297
1 parent b480997 commit b342edd

24 files changed

+2719
-57
lines changed

modules/sdk-coin-tao/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,16 @@
4343
"@bitgo/abstract-substrate": "^1.1.2",
4444
"@bitgo/sdk-core": "^28.22.0",
4545
"@bitgo/statics": "^50.22.0",
46+
"bignumber.js": "^9.1.2",
4647
"@polkadot/keyring": "13.3.1",
4748
"@polkadot/types": "14.1.1",
4849
"@polkadot/util": "13.3.1",
4950
"@polkadot/util-crypto": "13.3.1",
5051
"@substrate/txwrapper-core": "7.5.2",
5152
"@substrate/txwrapper-polkadot": "7.5.2",
52-
"bignumber.js": "^9.1.2",
5353
"bs58": "^4.0.1",
5454
"hi-base32": "^0.5.1",
55+
"joi": "^17.4.0",
5556
"lodash": "^4.17.15",
5657
"tweetnacl": "^1.0.3"
5758
},
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { BuildTransactionError } from '@bitgo/sdk-core';
2+
3+
export class AddressValidationError extends BuildTransactionError {
4+
constructor(malformedAddress: string) {
5+
super(`The address '${malformedAddress}' is not a well-formed dot address`);
6+
this.name = AddressValidationError.name;
7+
}
8+
}
9+
10+
export class InvalidFeeError extends BuildTransactionError {
11+
constructor(type?: string, expectedType?: string) {
12+
super(`The specified type: "${type}" is not valid. Please provide the type: "${expectedType}"`);
13+
this.name = InvalidFeeError.name;
14+
}
15+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import {
2+
AccountId,
3+
AddProxyArgs,
4+
AddProxyBatchCallArgs,
5+
StakeBatchCallPayee,
6+
StakeBatchCallPayeeAccount,
7+
StakeBatchCallPayeeController,
8+
StakeBatchCallPayeeStaked,
9+
StakeBatchCallPayeeStash,
10+
ProxyArgs,
11+
} from './iface';
12+
13+
/**
14+
* Returns true if value is of type AccountId, false otherwise.
15+
*
16+
* @param value The object to test.
17+
*
18+
* @return true if value is of type AccountId, false otherwise.
19+
*/
20+
export function isAccountId(value: string | AccountId): value is AccountId {
21+
return value.hasOwnProperty('id');
22+
}
23+
24+
/**
25+
* Extracts the proxy address being added from an add proxy batch call or an add proxy call.
26+
27+
* @param call A batched add proxy call or an add proxy call from which to extract the proxy
28+
* address.
29+
*
30+
* @return the proxy address being added from an add proxy batch call or an add proxy call.
31+
*/
32+
export function getDelegateAddress(call: AddProxyBatchCallArgs | AddProxyArgs): string {
33+
if (isAccountId(call.delegate)) {
34+
return call.delegate.id;
35+
} else {
36+
return call.delegate;
37+
}
38+
}
39+
40+
/**
41+
* Returns true if value is of type StakeBatchCallPayeeStaked, false otherwise.
42+
*
43+
* @param value The object to test.
44+
*
45+
* @return true if value is of type StakeBatchCallPayeeStaked, false otherwise.
46+
*/
47+
export function isStakeBatchCallPayeeStaked(value: StakeBatchCallPayee): value is StakeBatchCallPayeeStaked {
48+
return (value as StakeBatchCallPayeeStaked).hasOwnProperty('staked');
49+
}
50+
51+
/**
52+
* Returns true if value is of type StakeBatchCallPayeeStash, false otherwise.
53+
*
54+
* @param value The object to test.
55+
*
56+
* @return true if value is of type StakeBatchCallPayeeStash, false otherwise.
57+
*/
58+
export function isStakeBatchCallPayeeStash(value: StakeBatchCallPayee): value is StakeBatchCallPayeeStash {
59+
return (value as StakeBatchCallPayeeStash).hasOwnProperty('stash');
60+
}
61+
62+
/**
63+
* Returns true if value is of type StakeBatchCallPayeeController, false otherwise.
64+
*
65+
* @param value The object to test.
66+
*
67+
* @return true if value is of type StakeBatchCallPayeeController, false otherwise.
68+
*/
69+
export function isStakeBatchCallPayeeController(value: StakeBatchCallPayee): value is StakeBatchCallPayeeController {
70+
return (value as StakeBatchCallPayeeController).hasOwnProperty('controller');
71+
}
72+
73+
/**
74+
* Returns true if value is of type StakeBatchCallPayeeAccount, false otherwise.
75+
*
76+
* @param value The object to test.
77+
*
78+
* @return true if value is of type StakeBatchCallPayeeAccount, false otherwise.
79+
*/
80+
export function isStakeBatchCallPayeeAccount(value: StakeBatchCallPayee): value is StakeBatchCallPayeeAccount {
81+
return (
82+
(value as StakeBatchCallPayeeAccount).account !== undefined &&
83+
(value as StakeBatchCallPayeeAccount).account !== null
84+
);
85+
}
86+
87+
/**
88+
* Extracts the proxy address being added from ProxyArgs.
89+
90+
* @param args the ProxyArgs object from which to extract the proxy address.
91+
*
92+
* @return the proxy address being added.
93+
*/
94+
export function getAddress(args: ProxyArgs): string {
95+
if (isAccountId(args.real)) {
96+
return args.real.id;
97+
} else {
98+
return args.real;
99+
}
100+
}

modules/sdk-coin-tao/src/lib/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ export { Transaction } from './transaction';
66
export { TransactionBuilder } from './transactionBuilder';
77
export { TransferBuilder } from './transferBuilder';
88
export { TransactionBuilderFactory } from './transactionBuilderFactory';
9+
export { SingletonRegistry } from './singletonRegistry';
10+
export { NativeTransferBuilder } from './nativeTransferBuilder';
911
export { Interface, Utils };
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import { BaseAddress, DotAssetTypes, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { methods } from '@substrate/txwrapper-polkadot';
4+
import { DecodedSignedTx, DecodedSigningPayload, UnsignedTransaction } from '@substrate/txwrapper-core';
5+
import BigNumber from 'bignumber.js';
6+
import { MethodNames, ProxyArgs, ProxyType, TransferAllArgs, TransferArgs } from './iface';
7+
import { getAddress } from './iface_utils';
8+
import { SingletonRegistry } from './singletonRegistry';
9+
import { Transaction } from './transaction';
10+
import { TransactionBuilder } from './transactionBuilder';
11+
import { ProxyTransactionSchema, TransferAllTransactionSchema, TransferTransactionSchema } from './txnSchema';
12+
import utils from './utils';
13+
14+
export abstract class NativeTransferBuilder extends TransactionBuilder {
15+
protected _sweepFreeBalance = false;
16+
protected _keepAddressAlive = true;
17+
protected _amount: string;
18+
protected _to: string;
19+
protected _owner: string;
20+
protected _forceProxyType: ProxyType;
21+
22+
constructor(_coinConfig: Readonly<CoinConfig>) {
23+
super(_coinConfig);
24+
}
25+
26+
/**
27+
*
28+
* Dispatch the given call from an account that the sender is authorised for through add_proxy.
29+
*
30+
* @returns {UnsignedTransaction} an unsigned Dot transaction
31+
*
32+
* @see https://polkadot.js.org/docs/substrate/extrinsics/#proxy
33+
*/
34+
protected buildTransaction(): UnsignedTransaction {
35+
const baseTxInfo = this.createBaseTxInfo();
36+
let transferTx;
37+
if (this._sweepFreeBalance) {
38+
transferTx = methods.balances.transferAll(
39+
{
40+
dest: { id: this._to },
41+
keepAlive: this._keepAddressAlive,
42+
},
43+
baseTxInfo.baseTxInfo,
44+
baseTxInfo.options
45+
);
46+
} else {
47+
transferTx = methods.balances.transferKeepAlive(
48+
{
49+
value: this._amount,
50+
dest: { id: this._to },
51+
},
52+
baseTxInfo.baseTxInfo,
53+
baseTxInfo.options
54+
);
55+
}
56+
57+
if (!this._owner) {
58+
return transferTx;
59+
}
60+
return methods.proxy.proxy(
61+
{
62+
real: this._owner,
63+
forceProxyType: this._forceProxyType,
64+
call: transferTx.method,
65+
},
66+
baseTxInfo.baseTxInfo,
67+
baseTxInfo.options
68+
);
69+
}
70+
71+
protected get transactionType(): TransactionType {
72+
return TransactionType.Send;
73+
}
74+
75+
/**
76+
*
77+
* Set this to be a sweep transaction, using TransferAll with keepAlive set to true by default.
78+
* If keepAlive is false, the entire address will be swept (including the 1 DOT minimum).
79+
*
80+
* @param {boolean} keepAlive - keep the address alive after this sweep
81+
* @returns {TransferBuilder} This transfer builder.
82+
*
83+
* @see https://github.com/paritytech/txwrapper-core/blob/main/docs/modules/txwrapper_substrate_src.methods.balances.md#transferall
84+
*/
85+
sweep(keepAlive?: boolean): this {
86+
this._sweepFreeBalance = true;
87+
if (keepAlive !== undefined) {
88+
this._keepAddressAlive = keepAlive;
89+
}
90+
return this;
91+
}
92+
93+
/**
94+
*
95+
* The amount for transfer transaction.
96+
*
97+
* @param {string} amount
98+
* @returns {TransferBuilder} This transfer builder.
99+
*
100+
* @see https://wiki.polkadot.network/docs/build-protocol-info
101+
*/
102+
amount(amount: string): this {
103+
this.validateValue(new BigNumber(amount));
104+
this._amount = amount;
105+
return this;
106+
}
107+
108+
/**
109+
*
110+
* The destination address for transfer transaction.
111+
*
112+
* @param {string} dest
113+
* @returns {TransferBuilder} This transfer builder.
114+
*
115+
* @see https://wiki.polkadot.network/docs/build-protocol-info
116+
*/
117+
to({ address }: BaseAddress): this {
118+
this.validateAddress({ address });
119+
this._to = address;
120+
return this;
121+
}
122+
123+
/**
124+
*
125+
* The real address of the original tx
126+
*
127+
* @param {BaseAddress} real
128+
* @returns {TransferBuilder} This builder.
129+
*
130+
* @see https://wiki.polkadot.network/docs/learn-proxies#why-use-a-proxy
131+
*/
132+
owner(owner: BaseAddress): this {
133+
this.validateAddress({ address: owner.address });
134+
this._owner = owner.address;
135+
return this;
136+
}
137+
138+
/**
139+
*
140+
* The proxy type to execute
141+
*
142+
* @param {proxyType} forceProxyType
143+
* @returns {TransferBuilder} This builder.
144+
*
145+
* @see https://wiki.polkadot.network/docs/learn-proxies#proxy-types
146+
*/
147+
forceProxyType(forceProxyType: ProxyType): this {
148+
this._forceProxyType = forceProxyType;
149+
return this;
150+
}
151+
152+
/** @inheritdoc */
153+
validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx, rawTransaction: string): void {
154+
if (decodedTxn.method?.name === MethodNames.TransferKeepAlive) {
155+
const txMethod = decodedTxn.method.args as unknown as TransferArgs;
156+
const amount = `${txMethod.value}`;
157+
const to = txMethod.dest.id;
158+
const validationResult = TransferTransactionSchema.validate({ amount, to });
159+
if (validationResult.error) {
160+
throw new InvalidTransactionError(`Transfer Transaction validation failed: ${validationResult.error.message}`);
161+
}
162+
} else if (decodedTxn.method?.name === MethodNames.Proxy) {
163+
const txMethod = decodedTxn.method.args as unknown as ProxyArgs;
164+
const real = getAddress(txMethod);
165+
const forceProxyType = txMethod.forceProxyType;
166+
const decodedCall = utils.decodeCallMethod(rawTransaction, {
167+
registry: SingletonRegistry.getInstance(this._material),
168+
metadataRpc: this._material.metadata,
169+
});
170+
const amount = `${decodedCall.value}`;
171+
const to = decodedCall.dest.id;
172+
const validationResult = ProxyTransactionSchema.validate({ real, forceProxyType, amount, to });
173+
if (validationResult.error) {
174+
throw new InvalidTransactionError(`Proxy Transaction validation failed: ${validationResult.error.message}`);
175+
}
176+
}
177+
}
178+
179+
/** @inheritdoc */
180+
protected fromImplementation(rawTransaction: string): Transaction {
181+
const tx = super.fromImplementation(rawTransaction);
182+
if (this._method?.name === MethodNames.TransferKeepAlive) {
183+
const txMethod = this._method.args as TransferArgs;
184+
this.amount(txMethod.value);
185+
this.to({
186+
address: utils.decodeDotAddress(
187+
txMethod.dest.id,
188+
utils.getAddressFormat(this._coinConfig.name as DotAssetTypes)
189+
),
190+
});
191+
} else if (this._method?.name === MethodNames.TransferAll) {
192+
this._sweepFreeBalance = true;
193+
const txMethod = this._method.args as TransferAllArgs;
194+
this.sweep(txMethod.keepAlive);
195+
this.to({
196+
address: utils.decodeDotAddress(
197+
txMethod.dest.id,
198+
utils.getAddressFormat(this._coinConfig.name as DotAssetTypes)
199+
),
200+
});
201+
} else if (this._method?.name === MethodNames.Proxy) {
202+
const txMethod = this._method.args as ProxyArgs;
203+
this.owner({
204+
address: utils.decodeDotAddress(
205+
getAddress(txMethod),
206+
utils.getAddressFormat(this._coinConfig.name as DotAssetTypes)
207+
),
208+
});
209+
this.forceProxyType(txMethod.forceProxyType);
210+
const decodedCall = utils.decodeCallMethod(rawTransaction, {
211+
registry: SingletonRegistry.getInstance(this._material),
212+
metadataRpc: this._material.metadata,
213+
});
214+
if (!decodedCall.value || !decodedCall.dest) {
215+
throw new InvalidTransactionError(
216+
`Invalid Proxy Transaction Method: ${this._method?.name}. Expected transferKeepAlive`
217+
);
218+
}
219+
this.amount(`${decodedCall.value}`);
220+
this.to({
221+
address: utils.decodeDotAddress(
222+
decodedCall.dest.id,
223+
utils.getAddressFormat(this._coinConfig.name as DotAssetTypes)
224+
),
225+
});
226+
} else {
227+
throw new InvalidTransactionError(
228+
`Invalid Transaction Type: ${this._method?.name}. Expected a transferKeepAlive or a proxy transferKeepAlive transaction`
229+
);
230+
}
231+
return tx;
232+
}
233+
234+
/** @inheritdoc */
235+
validateTransaction(_: Transaction): void {
236+
super.validateTransaction(_);
237+
this.validateFields(this._to, this._amount, this._owner, this._forceProxyType);
238+
}
239+
240+
private validateFields(to: string, amount: string, real?: string, forceProxyType?: string): void {
241+
let validationResult;
242+
if (forceProxyType) {
243+
validationResult = ProxyTransactionSchema.validate({ to, amount, real, forceProxyType });
244+
} else if (this._sweepFreeBalance) {
245+
validationResult = TransferAllTransactionSchema.validate({ to });
246+
} else {
247+
validationResult = TransferTransactionSchema.validate({ amount, to });
248+
}
249+
250+
if (validationResult.error) {
251+
throw new InvalidTransactionError(
252+
`Proxy/TransferAll/TransferKeepAlive Transaction validation failed: ${validationResult.error.message}`
253+
);
254+
}
255+
}
256+
}

0 commit comments

Comments
 (0)