-
Notifications
You must be signed in to change notification settings - Fork 284
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement base transaction and transfer builders
TICKET: WIN-4297
- Loading branch information
1 parent
b480997
commit a78a025
Showing
25 changed files
with
2,933 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
208 changes: 208 additions & 0 deletions
208
modules/sdk-coin-tao/src/lib/addressInitializationBuilder.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
import { BaseAddress, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core'; | ||
import { BaseCoin as CoinConfig } from '@bitgo/statics'; | ||
import { DecodedSignedTx, DecodedSigningPayload, UnsignedTransaction } from '@substrate/txwrapper-core'; | ||
import { methods } from '@substrate/txwrapper-polkadot'; | ||
import BigNumber from 'bignumber.js'; | ||
import { ValidationResult } from 'joi'; | ||
import { AddAnonymousProxyArgs, AddProxyArgs, MethodNames, ProxyType } from './iface'; | ||
import { getDelegateAddress } from './iface_utils'; | ||
import { Transaction } from './transaction'; | ||
import { TransactionBuilder } from './transactionBuilder'; | ||
import { AddressInitializationSchema, AnonymousAddressInitializationSchema } from './txnSchema'; | ||
import utils from './utils'; | ||
|
||
export class AddressInitializationBuilder extends TransactionBuilder { | ||
protected _delegate: string; | ||
protected _proxyType: ProxyType; | ||
protected _delay: string; | ||
protected _index = 0; | ||
|
||
constructor(_coinConfig: Readonly<CoinConfig>) { | ||
super(_coinConfig); | ||
} | ||
|
||
/** @inheritDoc */ | ||
protected buildTransaction(): UnsignedTransaction { | ||
if (this._delegate) { | ||
return this.buildAddProxyTransaction(); | ||
} else { | ||
return this.buildAnonymousProxyTransaction(); | ||
} | ||
} | ||
|
||
/** | ||
* Register a proxy account for the sender that is able to make calls on its behalf. | ||
* | ||
* @returns {UnsignedTransaction} an unsigned Dot transaction | ||
* | ||
* @see https://polkadot.js.org/docs/substrate/extrinsics/#proxy | ||
*/ | ||
protected buildAddProxyTransaction(): UnsignedTransaction { | ||
const baseTxInfo = this.createBaseTxInfo(); | ||
return methods.proxy.addProxy( | ||
{ | ||
delegate: this._delegate, | ||
proxyType: this._proxyType, | ||
delay: this._delay, | ||
}, | ||
baseTxInfo.baseTxInfo, | ||
baseTxInfo.options | ||
); | ||
} | ||
|
||
/** | ||
* Spawn a receive address for the sender | ||
* | ||
* @return {UnsignedTransaction} an unsigned Dot transaction | ||
*/ | ||
protected buildAnonymousProxyTransaction(): UnsignedTransaction { | ||
const baseTxInfo = this.createBaseTxInfo(); | ||
return utils.pureProxy( | ||
{ | ||
proxyType: this._proxyType, | ||
index: this._index, | ||
delay: parseInt(this._delay, 10), | ||
}, | ||
baseTxInfo.baseTxInfo, | ||
baseTxInfo.options | ||
); | ||
} | ||
|
||
protected get transactionType(): TransactionType { | ||
return TransactionType.AddressInitialization; | ||
} | ||
|
||
/** | ||
* The account to delegate auth to. | ||
* | ||
* @param {BaseAddress} owner | ||
* @returns {AddressInitializationBuilder} This builder. | ||
* | ||
* @see https://wiki.polkadot.network/docs/learn-proxies#why-use-a-proxy | ||
*/ | ||
owner(owner: BaseAddress): this { | ||
this.validateAddress({ address: owner.address }); | ||
this._delegate = owner.address; | ||
return this; | ||
} | ||
|
||
/** | ||
* Used for disambiguation if multiple calls are made in the same transaction | ||
* Use 0 as a default | ||
* | ||
* @param {number} index | ||
* | ||
* @returns {AddressInitializationBuilder} This transfer builder. | ||
*/ | ||
index(index: number): this { | ||
this.validateValue(new BigNumber(index)); | ||
this._index = index; | ||
return this; | ||
} | ||
|
||
/** | ||
* The proxy type to add. | ||
* | ||
* @param {proxyType} proxyType | ||
* @returns {AddressInitializationBuilder} This builder. | ||
* | ||
* @see https://wiki.polkadot.network/docs/learn-proxies#proxy-types | ||
*/ | ||
type(proxyType: ProxyType): this { | ||
this._proxyType = proxyType; | ||
return this; | ||
} | ||
|
||
/** | ||
* The number of blocks that an announcement must be in place for. | ||
* before the corresponding call may be dispatched. | ||
* If zero, then no announcement is needed. | ||
* TODO: move to the validity window method once it has been standardized | ||
* | ||
* @param {string} delay | ||
* @returns {AddressInitializationBuilder} This transfer builder. | ||
* | ||
* @see https://wiki.polkadot.network/docs/learn-proxies#time-delayed-proxies | ||
*/ | ||
delay(delay: string): this { | ||
this.validateValue(new BigNumber(parseInt(delay, 10))); | ||
this._delay = delay; | ||
return this; | ||
} | ||
|
||
/** @inheritdoc */ | ||
validateDecodedTransaction(decodedTxn: DecodedSigningPayload | DecodedSignedTx): void { | ||
let validationResult; | ||
if (decodedTxn.method?.name === MethodNames.AddProxy) { | ||
const txMethod = decodedTxn.method.args as unknown as AddProxyArgs; | ||
validationResult = this.validateAddProxyFields(getDelegateAddress(txMethod), txMethod.proxyType, txMethod.delay); | ||
} else if (decodedTxn.method?.name === MethodNames.Anonymous || decodedTxn.method?.name === MethodNames.PureProxy) { | ||
const txMethod = decodedTxn.method.args as unknown as AddAnonymousProxyArgs; | ||
validationResult = this.validateAnonymousProxyFields( | ||
parseInt(txMethod.index, 10), | ||
txMethod.proxyType, | ||
txMethod.delay | ||
); | ||
} | ||
if (validationResult.error) { | ||
throw new InvalidTransactionError(`Transaction validation failed: ${validationResult.error.message}`); | ||
} | ||
} | ||
|
||
/** @inheritdoc */ | ||
protected fromImplementation(rawTransaction: string): Transaction { | ||
const tx = super.fromImplementation(rawTransaction); | ||
if (this._method?.name === MethodNames.AddProxy) { | ||
const txMethod = this._method.args as AddProxyArgs; | ||
this.owner({ address: getDelegateAddress(txMethod) }); | ||
this.type(txMethod.proxyType); | ||
this.delay(new BigNumber(txMethod.delay).toString()); | ||
} else if (this._method?.name === MethodNames.Anonymous || this._method?.name === MethodNames.PureProxy) { | ||
const txMethod = this._method.args as AddAnonymousProxyArgs; | ||
this.index(new BigNumber(txMethod.index).toNumber()); | ||
this.type(txMethod.proxyType); | ||
this.delay(new BigNumber(txMethod.delay).toString()); | ||
} else { | ||
throw new InvalidTransactionError( | ||
`Invalid Transaction Type: ${this._method?.name}. Expected ${MethodNames.AddProxy} or ${MethodNames.Anonymous}` | ||
); | ||
} | ||
return tx; | ||
} | ||
|
||
/** @inheritdoc */ | ||
validateTransaction(_: Transaction): void { | ||
super.validateTransaction(_); | ||
this.validateFields(); | ||
} | ||
|
||
private validateFields(): void { | ||
let validationResult: ValidationResult; | ||
if (this._delegate) { | ||
validationResult = this.validateAddProxyFields(this._delegate, this._proxyType, this._delay); | ||
} else { | ||
validationResult = this.validateAnonymousProxyFields(this._index, this._proxyType, this._delay); | ||
} | ||
if (validationResult.error) { | ||
throw new InvalidTransactionError( | ||
`AddressInitialization Transaction validation failed: ${validationResult.error.message}` | ||
); | ||
} | ||
} | ||
|
||
private validateAddProxyFields(delegate: string, proxyType: string, delay: string): ValidationResult { | ||
return AddressInitializationSchema.validate({ | ||
delegate, | ||
proxyType, | ||
delay, | ||
}); | ||
} | ||
|
||
private validateAnonymousProxyFields(index: number, proxyType: string, delay: string): ValidationResult { | ||
return AnonymousAddressInitializationSchema.validate({ | ||
proxyType, | ||
index, | ||
delay, | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.