Skip to content

Commit 444a052

Browse files
feat(sdk-coin-iota): transaction builder for iota
TICKET: WIN-6890
1 parent 87f4cd1 commit 444a052

File tree

18 files changed

+3215
-78
lines changed

18 files changed

+3215
-78
lines changed

modules/sdk-coin-iota/src/iota.ts

Lines changed: 142 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,33 @@ import {
33
BaseCoin,
44
BitGoBase,
55
KeyPair,
6-
ParseTransactionOptions,
76
ParsedTransaction,
87
SignTransactionOptions,
98
SignedTransaction,
109
VerifyTransactionOptions,
1110
MultisigType,
1211
multisigTypes,
1312
MPCAlgorithm,
13+
InvalidAddressError,
14+
EDDSAMethods,
1415
TssVerifyAddressOptions,
1516
MPCType,
16-
verifyEddsaTssWalletAddress,
17+
PopulatedIntent,
18+
PrebuildTransactionWithIntentOptions,
1719
} from '@bitgo/sdk-core';
18-
import { BaseCoin as StaticsBaseCoin, CoinFamily } from '@bitgo/statics';
20+
import { BaseCoin as StaticsBaseCoin, CoinFamily, coins } from '@bitgo/statics';
1921
import utils from './lib/utils';
20-
import { KeyPair as IotaKeyPair } from './lib';
22+
import { KeyPair as IotaKeyPair, Transaction, TransactionBuilderFactory } from './lib';
2123
import { auditEddsaPrivateKey } from '@bitgo/sdk-lib-mpc';
24+
import BigNumber from 'bignumber.js';
25+
import * as _ from 'lodash';
26+
import {
27+
ExplainTransactionOptions,
28+
IotaParseTransactionOptions,
29+
TransactionExplanation,
30+
TransferTxData,
31+
} from './lib/iface';
32+
import { TransferTransaction } from './lib/transferTransaction';
2233

2334
export class Iota extends BaseCoin {
2435
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
@@ -77,12 +88,44 @@ export class Iota extends BaseCoin {
7788
return utils.isValidAddress(address);
7889
}
7990

91+
/**
92+
* @inheritDoc
93+
*/
94+
async explainTransaction(params: ExplainTransactionOptions): Promise<TransactionExplanation> {
95+
const rawTx = params.txBase64;
96+
if (!rawTx) {
97+
throw new Error('missing required tx prebuild property txBase64');
98+
}
99+
const transaction = await this.rebuildTransaction(rawTx);
100+
if (!transaction) {
101+
throw new Error('failed to explain transaction');
102+
}
103+
return transaction.explainTransaction();
104+
}
105+
80106
/**
81107
* Verifies that a transaction prebuild complies with the original intention
82108
* @param params
83109
*/
84110
async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
85-
// TODO: Add IOTA-specific transaction verification logic
111+
const { txPrebuild: txPrebuild, txParams: txParams } = params;
112+
const rawTx = txPrebuild.txBase64;
113+
if (!rawTx) {
114+
throw new Error('missing required tx prebuild property txBase64');
115+
}
116+
const transaction = await this.rebuildTransaction(rawTx);
117+
if (!transaction) {
118+
throw new Error('failed to verify transaction');
119+
}
120+
if (txParams.recipients !== undefined) {
121+
if (!(transaction instanceof TransferTransaction)) {
122+
throw new Error('Tx not a transfer transaction');
123+
}
124+
const txData = transaction.toJson() as TransferTxData;
125+
if (!txData.recipients || !_.isEqual(txParams.recipients, txData.recipients)) {
126+
throw new Error('Tx recipients does not match with expected txParams recipients');
127+
}
128+
}
86129
return true;
87130
}
88131

@@ -91,20 +134,80 @@ export class Iota extends BaseCoin {
91134
* @param params
92135
*/
93136
async isWalletAddress(params: TssVerifyAddressOptions): Promise<boolean> {
94-
return verifyEddsaTssWalletAddress(
95-
params,
96-
(address) => this.isValidAddress(address),
97-
(publicKey) => utils.getAddressFromPublicKey(publicKey)
98-
);
137+
const { keychains, address, index } = params;
138+
139+
if (!this.isValidAddress(address)) {
140+
throw new InvalidAddressError(`invalid address: ${address}`);
141+
}
142+
143+
if (!keychains) {
144+
throw new Error('missing required param keychains');
145+
}
146+
147+
for (const keychain of keychains) {
148+
const MPC = await EDDSAMethods.getInitializedMpcInstance();
149+
const commonKeychain = keychain.commonKeychain as string;
150+
151+
const derivationPath = 'm/' + index;
152+
const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
153+
const expectedAddress = utils.getAddressFromPublicKey(derivedPublicKey);
154+
155+
if (address !== expectedAddress) {
156+
return false;
157+
}
158+
}
159+
return true;
99160
}
100161

101162
/**
102163
* Parse a transaction
103164
* @param params
104165
*/
105-
async parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
106-
// TODO: Add IOTA-specific transaction parsing logic
107-
return {};
166+
async parseTransaction(params: IotaParseTransactionOptions): Promise<ParsedTransaction> {
167+
const transactionExplanation = await this.explainTransaction({ txBase64: params.txBase64 });
168+
169+
if (!transactionExplanation) {
170+
throw new Error('Invalid transaction');
171+
}
172+
173+
let fee = new BigNumber(0);
174+
175+
if (transactionExplanation.outputs.length <= 0) {
176+
return {
177+
inputs: [],
178+
outputs: [],
179+
fee,
180+
};
181+
}
182+
183+
const senderAddress = transactionExplanation.outputs[0].address;
184+
if (transactionExplanation.fee.fee !== '') {
185+
fee = new BigNumber(transactionExplanation.fee.fee);
186+
}
187+
188+
// assume 1 sender, who is also the fee payer
189+
const inputs = [
190+
{
191+
address: senderAddress,
192+
amount: new BigNumber(transactionExplanation.outputAmount).plus(fee).toFixed(),
193+
},
194+
];
195+
196+
const outputs: {
197+
address: string;
198+
amount: string;
199+
}[] = transactionExplanation.outputs.map((output) => {
200+
return {
201+
address: output.address,
202+
amount: new BigNumber(output.amount).toFixed(),
203+
};
204+
});
205+
206+
return {
207+
inputs,
208+
outputs,
209+
fee,
210+
};
108211
}
109212

110213
/**
@@ -149,4 +252,30 @@ export class Iota extends BaseCoin {
149252
}
150253
auditEddsaPrivateKey(prv, publicKey ?? '');
151254
}
255+
256+
/** @inheritDoc */
257+
async getSignablePayload(serializedTx: string): Promise<Buffer> {
258+
const rebuiltTransaction = await this.rebuildTransaction(serializedTx);
259+
return rebuiltTransaction.signablePayload;
260+
}
261+
262+
/** inherited doc */
263+
setCoinSpecificFieldsInIntent(intent: PopulatedIntent, params: PrebuildTransactionWithIntentOptions): void {
264+
intent.unspents = params.unspents;
265+
}
266+
267+
private getTxBuilderFactory(): TransactionBuilderFactory {
268+
return new TransactionBuilderFactory(coins.get(this.getChain()));
269+
}
270+
271+
private async rebuildTransaction(txHex: string): Promise<Transaction> {
272+
const txBuilderFactory = this.getTxBuilderFactory();
273+
try {
274+
const txBuilder = txBuilderFactory.from(txHex);
275+
txBuilder.transaction.isSimulateTx = false;
276+
return (await txBuilder.build()) as Transaction;
277+
} catch {
278+
throw new Error('Failed to rebuild transaction');
279+
}
280+
}
152281
}

modules/sdk-coin-iota/src/lib/constants.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ export const IOTA_ADDRESS_LENGTH = 64;
22
export const IOTA_TRANSACTION_DIGEST_LENGTH = 32;
33
export const IOTA_BLOCK_DIGEST_LENGTH = 32;
44
export const IOTA_SIGNATURE_LENGTH = 64;
5-
export const ADDRESS_BYTES_LENGTH = 32;
6-
export const AMOUNT_BYTES_LENGTH = 8;
7-
export const SECONDS_PER_WEEK = 7 * 24 * 60 * 60; // 1 week in seconds
5+
export const IOTA_KEY_LENGTH = 64; // Ed25519 private key is 32 bytes = 64 hex characters
6+
export const IOTA_KEY_BYTES_LENGTH = 32; // Ed25519 public key is 32 bytes
7+
export const MAX_INPUT_OBJECTS = 2048;
8+
export const MAX_GAS_PAYMENT_OBJECTS = 256;
9+
export const MAX_GAS_BUDGET = 50000000000;
10+
export const MAX_GAS_PRICE = 100000;
11+
export const MAX_RECIPIENTS = 256; // Maximum number of recipients in a transfer transaction
12+
export const TRANSFER_TRANSACTION_COMMANDS = ['SplitCoins', 'MergeCoins', 'TransferObjects'];
Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,49 @@
1+
import {
2+
ParseTransactionOptions as BaseParseTransactionOptions,
3+
TransactionExplanation as BaseTransactionExplanation,
4+
TransactionRecipient,
5+
TransactionType as BitGoTransactionType,
6+
TransactionType,
7+
} from '@bitgo/sdk-core';
8+
9+
export interface TransactionExplanation extends BaseTransactionExplanation {
10+
type: BitGoTransactionType;
11+
}
12+
13+
export type TransactionObjectInput = {
14+
objectId: string;
15+
version: string;
16+
digest: string;
17+
};
18+
19+
export type GasData = {
20+
gasBudget?: number;
21+
gasPrice?: number;
22+
gasPaymentObjects?: TransactionObjectInput[];
23+
};
24+
125
/**
226
* The transaction data returned from the toJson() function of a transaction
327
*/
428
export interface TxData {
5-
id: string;
29+
id?: string;
30+
sender: string;
31+
gasBudget?: number;
32+
gasPrice?: number;
33+
gasPaymentObjects?: TransactionObjectInput[];
34+
gasSponsor?: string;
35+
type: TransactionType;
36+
}
37+
38+
export interface TransferTxData extends TxData {
39+
recipients: TransactionRecipient[];
40+
paymentObjects?: TransactionObjectInput[];
41+
}
42+
43+
export interface ExplainTransactionOptions {
44+
txBase64: string;
45+
}
46+
47+
export interface IotaParseTransactionOptions extends BaseParseTransactionOptions {
48+
txBase64: string;
649
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export { KeyPair } from './keyPair';
55
export { Transaction } from './transaction';
66
export { TransactionBuilder } from './transactionBuilder';
77
export { TransferBuilder } from './transferBuilder';
8+
export { TransferTransaction } from './transferTransaction';
89
export { TransactionBuilderFactory } from './transactionBuilderFactory';
910
export { Interface, Utils };

0 commit comments

Comments
 (0)