Skip to content

Commit

Permalink
Expose transaction crafting and estimation to third parties components (
Browse files Browse the repository at this point in the history
#6583)

* feat: add some tests

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: use fixtures everywhere

Signed-off-by: Stéphane Prohaszka <[email protected]>

* feat: use msw instead of mocking network lib calls

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: clean test

Signed-off-by: Stéphane Prohaszka <[email protected]>

* feat: separate core broadcast from bridge

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: add more tests

Signed-off-by: Stéphane Prohaszka <[email protected]>

* feat: expose estimateFees simpler version for Lama

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: separate more bridge dedicated logic from coin logic

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: separate bridge types from required ones to the core logic

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: add a lot of tests in signOperation

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: add test on getAccountShape

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: rename buildTransaction to better understanding of boundaries

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: add changeset

Signed-off-by: Stéphane Prohaszka <[email protected]>

* fix: lint

Signed-off-by: Stéphane Prohaszka <[email protected]>

* fix: revert export modifications

Signed-off-by: Stéphane Prohaszka <[email protected]>

* fix: link error in LLD

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: some cleanup

Signed-off-by: Stéphane Prohaszka <[email protected]>

* fix: revert unecessary modification

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: remove unecessary exposed test directory

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: try to solve ci test issue

Signed-off-by: Stéphane Prohaszka <[email protected]>

* chore: feedbacks and type enforcement

Signed-off-by: Stéphane Prohaszka <[email protected]>

* fix nano app to latest non-generic

* chore: revert changing on bot appcoin

Signed-off-by: Stéphane Prohaszka <[email protected]>

* fix: freshAdresses remove

Signed-off-by: Stéphane Prohaszka <[email protected]>

* fix: test flakiness and network cache

Signed-off-by: Stéphane Prohaszka <[email protected]>

---------

Signed-off-by: Stéphane Prohaszka <[email protected]>
Co-authored-by: Hedi EDELBLOUTE <[email protected]>
  • Loading branch information
sprohaszka-ledger and hedi-edelbloute committed Apr 29, 2024
1 parent f317536 commit 83e5690
Show file tree
Hide file tree
Showing 52 changed files with 2,431 additions and 843 deletions.
8 changes: 8 additions & 0 deletions .changeset/fair-onions-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@ledgerhq/coin-polkadot": minor
"@ledgerhq/types-live": patch
"@ledgerhq/live-common": patch
"@ledgerhq/coin-framework": patch
---

Expose Polkadot crafting functions to external components
145 changes: 145 additions & 0 deletions libs/coin-framework/src/account/pending.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { Account, Operation, TokenAccount } from "@ledgerhq/types-live";
import { addPendingOperation } from "./pending";
import BigNumber from "bignumber.js";
import { listCryptoCurrencies } from "@ledgerhq/cryptoassets/index";
import { TokenCurrency } from "@ledgerhq/types-cryptoassets";

describe("addPendingOperation", () => {
it("add the operation to the matching account Id", () => {
// Given
const account = createAccount("12");
const subAccount = createTokenAccount("12.1");
account.subAccounts = [subAccount];
const op = createOperation("12");

// When
const result = addPendingOperation(account, op);

// Then
expect(result).not.toBe(account);
expect(result.pendingOperations).toContain(op);
const resultSubAcc = result?.subAccounts;
expect(resultSubAcc).not.toBeFalsy();
expect(resultSubAcc).not.toBe(subAccount);
expect(resultSubAcc![0].pendingOperations).not.toContain(op);
});

it("add the operation to the matching subAccount Id", () => {
// Given
const account = createAccount("12");
const subAccount = createTokenAccount("12.1");
account.subAccounts = [subAccount];
const op = createOperation("12.1");

// When
const result = addPendingOperation(account, op);

// Then
expect(result).not.toBe(account);
expect(result.pendingOperations).not.toContain(op);
const resultSubAcc = result?.subAccounts;
expect(resultSubAcc).not.toBeFalsy();
expect(resultSubAcc).not.toBe(subAccount);
expect(resultSubAcc![0].pendingOperations).toContain(op);
});

it("lose an operation if the id doesn't match account and subaccount", () => {
// Given
const account = createAccount("12");
const subAccount = createTokenAccount("12.1");
account.subAccounts = [subAccount];
const op = createOperation("13");

// When
const result = addPendingOperation(account, op);

// Then
expect(result).not.toBe(account);
expect(result.pendingOperations).not.toContain(op);
const resultSubAcc = result?.subAccounts;
expect(resultSubAcc).not.toBeFalsy();
expect(resultSubAcc).not.toBe(subAccount);
expect(resultSubAcc![0].pendingOperations).not.toContain(op);
});
});

const emptyHistoryCache = {
HOUR: {
latestDate: null,
balances: [],
},
DAY: {
latestDate: null,
balances: [],
},
WEEK: {
latestDate: null,
balances: [],
},
};

function createAccount(id: string): Account {
const currency = listCryptoCurrencies(true)[0];

return {
type: "Account",
id,
seedIdentifier: "",
derivationMode: "",
index: 0,
freshAddress: "",
freshAddressPath: "",
name: "",
starred: false,
used: true,
balance: new BigNumber(0),
spendableBalance: new BigNumber(0),
creationDate: new Date(),
blockHeight: 0,
currency,
unit: currency.units[0],
operationsCount: 0,
operations: [],
pendingOperations: [],
lastSyncDate: new Date(),
// subAccounts: [],
balanceHistoryCache: emptyHistoryCache,
swapHistory: [],
};
}

function createTokenAccount(id: string): TokenAccount {
return {
type: "TokenAccount",
id,
parentId: "",
token: {} as unknown as TokenCurrency,
balance: new BigNumber(0),
spendableBalance: new BigNumber(0),
creationDate: new Date(),
operationsCount: 0,
operations: [],
pendingOperations: [],
starred: false,
balanceHistoryCache: emptyHistoryCache,
swapHistory: [],
};
}

function createOperation(accountId: string): Operation {
return {
id: "",
hash: "",
type: "ACTIVATE",
value: new BigNumber(0),
fee: new BigNumber(0),
// senders & recipients addresses
senders: [],
recipients: [],
blockHeight: undefined,
blockHash: undefined,
accountId,
date: new Date(),
extra: null,
};
}
21 changes: 11 additions & 10 deletions libs/coin-framework/src/account/pending.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Account, Operation, SubAccount } from "@ledgerhq/types-live";
import { getEnv } from "@ledgerhq/live-env";

export function shouldRetainPendingOperation(account: Account, op: Operation): boolean {
// FIXME: valueOf to compare dates in typescript
const delay = new Date().valueOf() - op.date.valueOf();
Expand All @@ -25,21 +26,21 @@ const appendPendingOp = (ops: Operation[], op: Operation) => {
return filtered;
};

function addInSubAccount(subaccounts: SubAccount[], op: Operation) {
const acc = subaccounts.find(sub => sub.id === op.accountId);

if (acc) {
const copy: SubAccount = { ...acc };
copy.pendingOperations = appendPendingOp(acc.pendingOperations, op);
subaccounts[subaccounts.indexOf(acc)] = copy;
}
}

export const addPendingOperation = (account: Account, operation: Operation): Account => {
const accountCopy = { ...account };
const { subOperations } = operation;
const { subAccounts } = account;

function addInSubAccount(subaccounts: SubAccount[], op: Operation) {
const acc = subaccounts.find(sub => sub.id === op.accountId);

if (acc) {
const copy: SubAccount = { ...acc };
copy.pendingOperations = appendPendingOp(acc.pendingOperations, op);
subaccounts[subaccounts.indexOf(acc)] = copy;
}
}

if (subOperations && subAccounts) {
const taCopy: SubAccount[] = subAccounts.slice(0);
subOperations.forEach(op => {
Expand Down
100 changes: 98 additions & 2 deletions libs/coin-framework/src/bridge/jsHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import BigNumber from "bignumber.js";
import { defaultUpdateTransaction } from "./jsHelpers";
import type { TransactionCommon } from "@ledgerhq/types-live";
import { AccountShapeInfo, defaultUpdateTransaction, makeSync } from "./jsHelpers";
import type { Account, SyncConfig, TransactionCommon } from "@ledgerhq/types-live";
import { listCryptoCurrencies } from "../currencies";
import { firstValueFrom } from "rxjs";

describe("jsHelpers", () => {
describe("defaultUpdateTransaction", () => {
Expand Down Expand Up @@ -37,4 +39,98 @@ describe("jsHelpers", () => {
});
});
});

describe("makeSync", () => {
it("returns a function to update account that give a new instance of account", async () => {
// Given
const account = createAccount("12");

// When
const accountUpdater = makeSync({
getAccountShape: (_accountShape: AccountShapeInfo) => Promise.resolve({} as Account),
})(account, {} as SyncConfig);
const updater = await firstValueFrom(accountUpdater);
const newAccount = updater(account);

// Then
const nonUpdatedFields = {
...account,
id: expect.any(String),
creationDate: expect.any(Date),
lastSyncDate: expect.any(Date),
subAccounts: undefined,
};
expect(newAccount).toEqual(nonUpdatedFields);
expect(newAccount.id).not.toEqual(account.id);
expect(newAccount.creationDate).not.toEqual(account.creationDate);
expect(newAccount.lastSyncDate).not.toEqual(account.lastSyncDate);
});

it("returns a account with a corrected formatted id", async () => {
// Given
const account = createAccount("12");

// When
const accountUpdater = makeSync({
getAccountShape: (_accountShape: AccountShapeInfo) => Promise.resolve({} as Account),
})(account, {} as SyncConfig);
const updater = await firstValueFrom(accountUpdater);
const newAccount = updater(account);

// Then
const expectedAccount = {
...account,
id: "js:2:bitcoin::",
subAccounts: undefined,
};
expect(newAccount.id).toEqual(expectedAccount.id);
});
});
});

const emptyHistoryCache = {
HOUR: {
latestDate: null,
balances: [],
},
DAY: {
latestDate: null,
balances: [],
},
WEEK: {
latestDate: null,
balances: [],
},
};

// Call once for all tests the currencies. Relies on real implementation to check also consistency.
const bitcoinCurrency = listCryptoCurrencies(true).find(c => c.id === "bitcoin")!;
function createAccount(id: string): Account {
const currency = bitcoinCurrency;

return {
type: "Account",
id,
seedIdentifier: "",
derivationMode: "",
index: 0,
freshAddress: "",
freshAddressPath: "",
name: "",
starred: false,
used: true,
balance: new BigNumber(0),
spendableBalance: new BigNumber(0),
creationDate: new Date(),
blockHeight: 0,
currency,
unit: currency.units[0],
operationsCount: 0,
operations: [],
pendingOperations: [],
lastSyncDate: new Date(),
// subAccounts: [],
balanceHistoryCache: emptyHistoryCache,
swapHistory: [],
};
}
2 changes: 1 addition & 1 deletion libs/coin-framework/src/bridge/jsHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export const makeSync =
shouldMergeOps?: boolean;
}): AccountBridge<any>["sync"] =>
(initial, syncConfig): Observable<AccountUpdater> =>
Observable.create((o: Observer<(acc: Account) => Account>) => {
new Observable((o: Observer<(acc: Account) => Account>) => {
async function main() {
const accountId = encodeAccountId({
type: "js",
Expand Down
12 changes: 11 additions & 1 deletion libs/coin-modules/coin-polkadot/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,18 @@
"require": "./lib/bridge/transaction.js",
"default": "./lib-es/bridge/transaction.js"
},
"./utils": {
"require": "./lib/bridge/utils.js",
"default": "./lib-es/bridge/utils.js"
},
"./*": {
"require": "./lib/*.js",
"default": "./lib-es/*.js"
},
".": {
"require": "./lib/index.js",
"default": "./lib-es/index.js"
},
"./package.json": "./package.json"
},
"license": "Apache-2.0",
Expand Down Expand Up @@ -113,7 +121,9 @@
"@faker-js/faker": "^8.4.1",
"jest": "^29.7.0",
"msw": "^2.0.11",
"ts-jest": "^29.1.1"
"ts-jest": "^29.1.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"scripts": {
"clean": "rimraf lib lib-es",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createFixtureOperation } from "../types/model.fixture";
import { createFixtureOperation } from "../types/bridge.fixture";
import broadcast from "./broadcast";

const mockSubmitExtrinsic = jest.fn();
Expand Down
18 changes: 18 additions & 0 deletions libs/coin-modules/coin-polkadot/src/bridge/broadcast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Operation, SignedOperation } from "@ledgerhq/types-live";
import { patchOperationWithHash } from "@ledgerhq/coin-framework/operation";
import { broadcast as logicBroadcast } from "../logic";

/**
* Broadcast the signed transaction
* @param {signature: string, operation: string} signedOperation
*/
const broadcast = async ({
signedOperation: { signature, operation },
}: {
signedOperation: SignedOperation;
}): Promise<Operation> => {
const hash = await logicBroadcast(signature);
return patchOperationWithHash(operation, hash);
};

export default broadcast;
Loading

0 comments on commit 83e5690

Please sign in to comment.