Skip to content

feat(sdk-coin-near): added delegate transaction builder #6248

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
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
6 changes: 5 additions & 1 deletion modules/bitgo/src/v2/coinFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AdaToken } from '@bitgo/sdk-coin-ada';
import { AlgoToken } from '@bitgo/sdk-coin-algo';
import { Bcha, Tbcha } from '@bitgo/sdk-coin-bcha';
import { HbarToken } from '@bitgo/sdk-coin-hbar';
import { Near, TNear } from '@bitgo/sdk-coin-near';
import { Near, TNear, Nep141Token } from '@bitgo/sdk-coin-near';
import { SolToken } from '@bitgo/sdk-coin-sol';
import { TrxToken } from '@bitgo/sdk-coin-trx';
import { CoinFactory } from '@bitgo/sdk-core';
Expand Down Expand Up @@ -451,6 +451,10 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin
Sip10Token.createTokenConstructors([...tokens.bitcoin.stx.tokens, ...tokens.testnet.stx.tokens]).forEach(
({ name, coinConstructor }) => coinFactory.register(name, coinConstructor)
);

Nep141Token.createTokenConstructors([...tokens.bitcoin.near.tokens, ...tokens.testnet.near.tokens]).forEach(
({ name, coinConstructor }) => coinFactory.register(name, coinConstructor)
);
}

export const GlobalCoinFactory: CoinFactory = new CoinFactory();
Expand Down
1 change: 1 addition & 0 deletions modules/bitgo/test/browser/browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ describe('Coins', () => {
Polyx: 1,
Tpolyx: 1,
CoredaoToken: 1,
Nep141Token: 1,
};
Object.keys(BitGoJS.Coin)
.filter((coinName) => !excludedKeys[coinName])
Expand Down
5 changes: 3 additions & 2 deletions modules/sdk-coin-near/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@
"dependencies": {
"@bitgo/sdk-core": "^35.0.0",
"@bitgo/statics": "^54.0.0",
"@near-js/crypto": "^2.0.1",
"@near-js/transactions": "^2.0.1",
"@stablelib/hex": "^1.0.0",
"bignumber.js": "^9.0.0",
"bn.js": "^5.2.1",
"bs58": "^4.0.1",
"js-sha256": "^0.9.0",
"lodash": "^4.17.14",
"near-api-js": "^0.44.2",
"near-api-js": "^5.1.1",
"superagent": "^9.0.1",
"tweetnacl": "^1.0.3"
},
Expand Down
244 changes: 244 additions & 0 deletions modules/sdk-coin-near/src/lib/abstractDelegateBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import assert from 'assert';
import BigNumber from 'bignumber.js';

import * as hex from '@stablelib/hex';
import * as nearAPI from 'near-api-js';
import { DelegateAction } from '@near-js/transactions';

import {
BaseAddress,
BaseKey,
BaseTransactionBuilder,
BuildTransactionError,
PublicKey as BasePublicKey,
Signature,
} from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';

import { AddressValidationError } from './errors';
import { BLOCK_HEIGHT_TTL } from './constants';
import { DelegateTransaction } from './delegateTransaction';
import { InitializableBuilder } from './initializableBuilder';
import { KeyPair } from './keyPair';
import utils from './utils';

export abstract class AbstractDelegateBuilder extends BaseTransactionBuilder implements InitializableBuilder {
private _delegateTransaction: DelegateTransaction;

private _sender: string;
private _publicKey: string;
protected _receiverId: string;
private _nonce: bigint;
private _recentBlockHeight: bigint;
private _signer: KeyPair;
private _signatures: Signature[] = [];
protected _actions: nearAPI.transactions.Action[];

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._delegateTransaction = new DelegateTransaction(_coinConfig);
}

/**
* Initialize the transaction builder fields using the decoded transaction data
*
* @param {Transaction} tx the transaction data
*/
initBuilder(tx: DelegateTransaction): void {
this._delegateTransaction = tx;
const nearDelegateAction = tx.nearTransaction;
this._sender = nearDelegateAction.senderId;
this._nonce = nearDelegateAction.nonce;
this._receiverId = nearDelegateAction.receiverId;
if (nearDelegateAction.publicKey.ed25519Key?.data) {
this._publicKey = hex.encode(nearDelegateAction.publicKey.ed25519Key.data);
}
this._recentBlockHeight = nearDelegateAction.maxBlockHeight;
this._actions = nearDelegateAction.actions;
}

/** @inheritdoc */
protected fromImplementation(rawTransaction: string): DelegateTransaction {
this.validateRawTransaction(rawTransaction);
this.buildImplementation();
return this.transaction;
}

/** @inheritdoc */
protected async buildImplementation(): Promise<DelegateTransaction> {
this.transaction.nearTransaction = this.buildNearTransaction();
if (this._signer) {
this.transaction.sign(this._signer);
}
if (this._signatures?.length > 0) {
this.transaction.constructSignedPayload(this._signatures[0].signature);
}
this.transaction.loadInputsAndOutputs();
return this.transaction;
}

/** @inheritdoc */
protected signImplementation(key: BaseKey): DelegateTransaction {
this._signer = new KeyPair({ prv: key.key });
return this._delegateTransaction;
}

/** @inheritdoc */
protected get transaction(): DelegateTransaction {
return this._delegateTransaction;
}

/** @inheritdoc */
protected set transaction(transaction: DelegateTransaction) {
this._delegateTransaction = transaction;
}

/** @inheritdoc */
validateAddress(address: BaseAddress, addressFormat?: string): void {
if (!utils.isValidAddress(address.address)) {
throw new AddressValidationError(address.address);
}
}

/** @inheritdoc */
validateKey(key: BaseKey): void {
try {
new KeyPair({ prv: key.key });
} catch {
throw new BuildTransactionError(`Key validation failed`);
}
}

/** @inheritdoc */
validateRawTransaction(rawTransaction: any): void {
try {
nearAPI.utils.serialize.deserialize(nearAPI.transactions.SCHEMA.SignedDelegate, rawTransaction);
} catch {
try {
nearAPI.utils.serialize.deserialize(nearAPI.transactions.SCHEMA.DelegateAction, rawTransaction);
} catch {
throw new BuildTransactionError('invalid raw transaction');
}
}
}

/** @inheritdoc */
validateTransaction(transaction: DelegateTransaction): void {
if (!transaction.nearTransaction) {
return;
}
this.validateAddress({ address: transaction.nearTransaction.senderId });
this.validateAddress({ address: transaction.nearTransaction.receiverId });
}

/** @inheritdoc */
validateValue(value: BigNumber): void {
if (value.isLessThan(0)) {
throw new BuildTransactionError('Value cannot be less than zero');
}
}

/**
* Sets the public key and the address of the sender of this transaction.
*
* @param {string} address the account that is sending this transaction
* @param {string} pubKey the public key that is sending this transaction
* @returns {AbstractDelegateBuilder} The delegate transaction builder
*/
public sender(address: string, pubKey: string): this {
if (!address || !utils.isValidAddress(address.toString())) {
throw new BuildTransactionError('Invalid or missing address, got: ' + address);
}
if (!pubKey || !utils.isValidPublicKey(pubKey)) {
throw new BuildTransactionError('Invalid or missing pubKey, got: ' + pubKey);
}
this._sender = address;
this._publicKey = pubKey;
return this;
}

/**
* Sets the account id of the receiver of this transaction.
*
* @param {string} accountId the account id of the account that is receiving this transaction
* @returns {AbstractDelegateBuilder} The delegate transaction builder
*/
public receiverId(accountId: string): this {
utils.isValidAddress(accountId);
this._receiverId = accountId;
return this;
}

/**
* Set the nonce
*
* @param {bigint} nonce - number that can be only used once
* @returns {AbstractDelegateBuilder} This delegate transaction builder
*/
public nonce(nonce: bigint): this {
if (nonce < 0) {
throw new BuildTransactionError(`Invalid nonce: ${nonce}`);
}
this._nonce = nonce;
return this;
}

/**
* Sets the recent block height for this transaction
*
* @param {string} blockHeight the blockHeight of this transaction
* @returns {AbstractDelegateBuilder} The delegate transaction builder
*/
public recentBlockHeight(blockHeight: bigint): this {
this._recentBlockHeight = blockHeight;
return this;
}

/**
* Sets the list of actions of this transaction.
*
* @param {nearAPI.transactions.Action[]} value the list of actions
* @returns {AbstractDelegateBuilder} The delegate transaction builder
*/
protected actions(value: nearAPI.transactions.Action[]): this {
this._actions = value;
return this;
}

/**
* Sets the action for this transaction/
*
* @param {nearAPI.transactions.Action} value the delegate action
* @returns {AbstractDelegateBuilder} The delegate transaction builder
*/
protected action(value: nearAPI.transactions.Action): this {
this._actions ? this._actions.push(value) : (this._actions = [value]);
return this;
}

/**
* Builds the NEAR delegate action.
*
* @return {DelegateAction} near sdk delegate action
*/
protected buildNearTransaction(): DelegateAction {
assert(this._sender, new BuildTransactionError('sender is required before building'));
assert(this._recentBlockHeight, new BuildTransactionError('recent block height is required before building'));

const tx = new DelegateAction({
senderId: this._sender,
receiverId: this._receiverId,
actions: this._actions,
nonce: this._nonce,
maxBlockHeight: BigInt(BLOCK_HEIGHT_TTL + this._recentBlockHeight),
publicKey: nearAPI.utils.PublicKey.fromString(nearAPI.utils.serialize.base_encode(hex.decode(this._publicKey))),
});

return tx;
}

/** @inheritDoc */
addSignature(publicKey: BasePublicKey, signature: Buffer): void {
this._signatures.push({ publicKey, signature });
}
}
7 changes: 7 additions & 0 deletions modules/sdk-coin-near/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,11 @@ export const StakingContractMethodNames = {
Withdraw: 'withdraw',
} as const;

export const AdditionalAllowedMethods = ['ft_transfer', 'storage_deposit'];

export const FT_TRANSFER = 'ft_transfer';
export const STORAGE_DEPOSIT = 'storage_deposit';

export const HEX_REGEX = /^[0-9a-fA-F]+$/;

export const BLOCK_HEIGHT_TTL = 120n;
12 changes: 6 additions & 6 deletions modules/sdk-coin-near/src/lib/contractCallWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { FunctionCall } from './iface';
*/
export class ContractCallWrapper {
private _methodName: string;
private _args: Record<string, unknown>;
private _args: Record<string, unknown> = {};
private _gas: string;
private _deposit: string;

Expand All @@ -21,35 +21,35 @@ export class ContractCallWrapper {
return this._methodName;
}

/** Set gas, expresed on yocto */
/** Set gas, expressed on yocto */
public set gas(gas: string) {
if (!this.isValidAmount(new BigNumber(gas))) {
throw new InvalidParameterValueError('Invalid gas value');
}
this._gas = gas;
}

/** Get gas, expresed on yocto*/
/** Get gas, expressed on yocto*/
public get gas(): string {
return this._gas;
}

/** Set deposit, expresed on yocto */
/** Set deposit, expressed on yocto */
public set deposit(deposit: string) {
if (!this.isValidAmount(new BigNumber(deposit))) {
throw new InvalidParameterValueError('Invalid deposit value');
}
this._deposit = deposit;
}

/** Get deposit, expresed on yocto */
/** Get deposit, expressed on yocto */
public get deposit(): string {
return this._deposit;
}

/** Get args, which are the parameters of a method */
public set args(args: Record<string, unknown>) {
this._args = args;
this._args = { ...this._args, ...args };
}

/** Set args, which are the parameters of a method */
Expand Down
Loading