Skip to content

Commit

Permalink
coin module boilerplate (#8809)
Browse files Browse the repository at this point in the history
new coin module boilerplate to help future integrations

---------

Co-authored-by: Hedi EDELBLOUTE <[email protected]>
  • Loading branch information
Wozacosta and hedi-edelbloute authored Jan 17, 2025
1 parent 03b94da commit ddf8d85
Show file tree
Hide file tree
Showing 56 changed files with 1,809 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/new-colts-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/coin-module-boilerplate": patch
---

feat: coin module boilerplate
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
PUB_KEY = "XPUB"
SECRET_KEY = "SECRET"
22 changes: 22 additions & 0 deletions libs/coin-modules/coin-module-boilerplate/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module.exports = {
env: {
browser: true,
es6: true,
},
overrides: [
{
files: ["src/**/*.test.{ts,tsx}"],
env: {
"jest/globals": true,
},
plugins: ["jest"],
},
],
rules: {
"no-console": ["error", { allow: ["warn", "error"] }],
"@typescript-eslint/no-empty-function": "off",
"no-empty-pattern": "off",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "warn",
},
};
1 change: 1 addition & 0 deletions libs/coin-modules/coin-module-boilerplate/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env.integ.test
29 changes: 29 additions & 0 deletions libs/coin-modules/coin-module-boilerplate/.unimportedrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"entry": [
"src/api/index.ts",
"src/bridge/index.ts",
"src/bridge/deviceTransactionConfig.ts",
"src/datasets/dataset-1.ts",
"src/signer/index.ts",
"src/test/index.ts",
"src/index.ts"
],
"ignorePatterns": [
"**/node_modules/**",
"**/*.fixture.ts",
"**/*.mock.ts",
"**/*.test.{js,jsx,ts,tsx}"
],
"ignoreUnresolved": [
"jest-get-type",
"jest-matcher-utils",
"jest-message-util"
],
"ignoreUnimported": [
"src/network/mock-network.ts",
"src/test/bot-specs.ts"
],
"ignoreUnused": [
"@ledgerhq/devices"
]
}
8 changes: 8 additions & 0 deletions libs/coin-modules/coin-module-boilerplate/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */
// `workerThreads: true` is required for validating object with `bigint` values
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testPathIgnorePatterns: ["lib/", "lib-es/", ".*\\.integ\\.test\\.[tj]s"],
workerThreads: true,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testRegex: ".integ.test.ts$",
testPathIgnorePatterns: ["lib/", "lib-es/"],
setupFiles: ["dotenv/config"],
};
136 changes: 136 additions & 0 deletions libs/coin-modules/coin-module-boilerplate/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
{
"name": "@ledgerhq/coin-module-boilerplate",
"version": "0.1.0",
"description": "Boilerplate coin integration",
"keywords": [
"Ledger",
"LedgerWallet",
"boilerplate",
"Hardware Wallet"
],
"repository": {
"type": "git",
"url": "https://github.com/LedgerHQ/ledger-live.git"
},
"bugs": {
"url": "https://github.com/LedgerHQ/ledger-live/issues"
},
"homepage": "https://github.com/LedgerHQ/ledger-live/tree/develop/libs/coin-modules/coin-module-boilerplate",
"publishConfig": {
"access": "public"
},
"typesVersions": {
"*": {
"lib/*": [
"lib/*"
],
"lib-es/*": [
"lib-es/*"
],
"api": [
"lib/api/index"
],
"deviceTransactionConfig": [
"lib/bridge/deviceTransactionConfig"
],
"logic": [
"lib/logic/index"
],
"specs": [
"lib/test/bot-specs"
],
"transaction": [
"lib/bridge/transaction"
],
"types": [
"lib/types/index"
],
"*": [
"lib/*",
"lib/bridge/*",
"lib/logic/*",
"lib/signer/*",
"lib/test/*",
"lib/types/*"
]
}
},
"exports": {
"./lib/*": "./lib/*.js",
"./lib-es/*": "./lib-es/*.js",
"./api": {
"require": "./lib/api/index.js",
"default": "./lib-es/api/index.js"
},
"./deviceTransactionConfig": {
"require": "./lib/bridge/deviceTransactionConfig.js",
"default": "./lib-es/bridge/deviceTransactionConfig.js"
},
"./logic": {
"require": "./lib/logic/index.js",
"default": "./lib-es/logic/index.js"
},
"./signer": {
"require": "./lib/signer/index.js",
"default": "./lib-es/signer/index.js"
},
"./specs": {
"require": "./lib/test/bot-specs.js",
"default": "./lib-es/test/bot-specs.js"
},
"./transaction": {
"require": "./lib/bridge/transaction.js",
"default": "./lib-es/bridge/transaction.js"
},
"./types": {
"require": "./lib/types/index.js",
"default": "./lib-es/types/index.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",
"dependencies": {
"@ledgerhq/coin-framework": "workspace:^",
"@ledgerhq/cryptoassets": "workspace:^",
"@ledgerhq/devices": "workspace:^",
"@ledgerhq/errors": "workspace:^",
"@ledgerhq/live-network": "workspace:^",
"@ledgerhq/live-env": "workspace:^",
"@ledgerhq/types-live": "workspace:^",
"bignumber.js": "^9.1.2",
"invariant": "^2.2.4",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@ledgerhq/types-cryptoassets": "workspace:^",
"@types/invariant": "^2.2.37",
"@types/jest": "^29.5.12",
"dotenv": "^16.4.5",
"expect": "^27.4.6",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"typescript": "^5.4.5"
},
"scripts": {
"clean": "rimraf lib lib-es",
"build": "tsc && tsc -m ES6 --outDir lib-es",
"coverage": "jest --coverage --testPathIgnorePatterns='/bridge.integration.test.ts|node_modules|lib-es|lib/' --passWithNoTests && mv coverage/coverage-final.json coverage/coverage-boilerplate.json",
"prewatch": "pnpm build",
"watch": "tsc --watch",
"doc": "documentation readme src/** --section=API --pe ts --re ts --re d.ts",
"lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache",
"lint:fix": "pnpm lint --fix",
"test": "jest",
"test-integ": "DOTENV_CONFIG_PATH=.env.integ.test jest --config=jest.integ.config.js",
"typecheck": "tsc --noEmit",
"unimported": "unimported"
}
}
15 changes: 15 additions & 0 deletions libs/coin-modules/coin-module-boilerplate/src/api/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createApi } from ".";
import { BoilerplateConfig } from "../config";

describe("createApi", () => {
it("should return every api methods", () => {
const api = createApi({} as BoilerplateConfig);
expect(api.broadcast).toBeDefined();
expect(api.combine).toBeDefined();
expect(api.craftTransaction).toBeDefined();
expect(api.estimateFees).toBeDefined();
expect(api.getBalance).toBeDefined();
expect(api.lastBlock).toBeDefined();
expect(api.listOperations).toBeDefined();
});
});
56 changes: 56 additions & 0 deletions libs/coin-modules/coin-module-boilerplate/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Api } from "@ledgerhq/coin-framework/api/index";
import coinConfig, { type BoilerplateConfig } from "../config";
import {
broadcast,
combine,
craftTransaction,
estimateFees,
getBalance,
getNextValidSequence,
lastBlock,
listOperations,
} from "../common-logic";
import BigNumber from "bignumber.js";

export function createApi(config: BoilerplateConfig): Api {
coinConfig.setCoinConfig(() => ({ ...config, status: { type: "active" } }));

return {
broadcast,
combine,
craftTransaction: craft,
estimateFees: estimate,
getBalance,
lastBlock,
listOperations,
};
}

async function craft(
address: string,
transaction: {
recipient: string;
amount: bigint;
fee: bigint;
},
): Promise<string> {
const nextSequenceNumber = await getNextValidSequence(address);
const tx = await craftTransaction(
{ address, nextSequenceNumber },
{
recipient: transaction.recipient,
amount: new BigNumber(transaction.amount.toString()),
fee: new BigNumber(transaction.fee.toString()),
},
);
return tx.serializedTransaction;
}

//
async function estimate(addr: string, amount: bigint): Promise<bigint> {
const { serializedTransaction } = await craftTransaction(
{ address: addr },
{ amount: new BigNumber(amount.toString()) },
);
return BigInt((await estimateFees(serializedTransaction)).toString());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Account, BroadcastArg } from "@ledgerhq/types-live";
import { broadcast } from "./broadcast";
jest.mock("@ledgerhq/coin-framework/operation");
jest.mock("../common-logic");
import { patchOperationWithHash } from "@ledgerhq/coin-framework/operation";
import { broadcast as broadcastLogic } from "../common-logic";

describe("broadcast", () => {
let patchOperationSpy: jest.SpyInstance;
let broadcastSpy: jest.SpyInstance;
beforeEach(() => {
patchOperationSpy = jest.spyOn({ patchOperationWithHash }, "patchOperationWithHash");
broadcastSpy = jest.spyOn({ broadcastLogic }, "broadcastLogic");
broadcastSpy.mockResolvedValue("hash");
});

it("should broadcast", () => {
broadcast({
signedOperation: {
signature: undefined,
operation: undefined,
},
} as unknown as BroadcastArg<Account>);
expect(broadcastLogic).toHaveBeenCalledTimes(1);
});

it("should patch operation with hash", () => {
broadcast({
signedOperation: {
signature: undefined,
operation: undefined,
},
} as unknown as BroadcastArg<Account>);
expect(patchOperationSpy).toHaveBeenCalledWith(undefined, "hash");
});
});
11 changes: 11 additions & 0 deletions libs/coin-modules/coin-module-boilerplate/src/bridge/broadcast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { AccountBridge } from "@ledgerhq/types-live";
import { patchOperationWithHash } from "@ledgerhq/coin-framework/operation";
import { broadcast as broadcastLogic } from "../common-logic";
import { Transaction } from "../types";

export const broadcast: AccountBridge<Transaction>["broadcast"] = async ({
signedOperation: { signature, operation },
}) => {
const hash = await broadcastLogic(signature);
return patchOperationWithHash(operation, hash);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Account, AccountLike } from "@ledgerhq/types-live";
import { createTransaction } from "./createTransaction";

describe("createTransaction", () => {
it("should create a 0 amount transaction", () => {
expect(createTransaction({} as AccountLike<Account>).amount.toNumber()).toEqual(0);
});

it("should create a transaction with boilerplate family", () => {
expect(createTransaction({} as AccountLike<Account>).family).toEqual("boilerplate");
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import BigNumber from "bignumber.js";
import { Transaction } from "../types";
import { AccountBridge } from "@ledgerhq/types-live";

// We create an empty transaction that will be filled later
export const createTransaction: AccountBridge<Transaction>["createTransaction"] = () => ({
family: "boilerplate",
amount: new BigNumber(0),
recipient: "",
fee: null,
memo: undefined,
networkInfo: null,
feeCustomUnit: null,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import getDeviceTransactionConfig from "./deviceTransactionConfig";
import BigNumber from "bignumber.js";

describe("getDeviceTransactionConfig", () => {
it("should return amount field when it's more than 0", () => {
expect(
getDeviceTransactionConfig({
transaction: {},
status: { amount: new BigNumber(1), estimatedFees: new BigNumber(0) },
} as any)[0],
).toEqual({ type: "amount", label: "Amount" });
});

it("should return fee field when it's more than 0", () => {
expect(
getDeviceTransactionConfig({
transaction: {},
status: { amount: new BigNumber(0), estimatedFees: new BigNumber(1) },
} as any)[0],
).toEqual({ type: "fees", label: "Fees" });
});
});
Loading

0 comments on commit ddf8d85

Please sign in to comment.