@ledgerhq/coin-xrp 0.2.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.js +20 -0
- package/.turbo/turbo-build.log +4 -0
- package/.unimportedrc.json +27 -0
- package/CHANGELOG.md +19 -0
- package/LICENSE.txt +21 -0
- package/jest.config.js +6 -0
- package/lib/api/index.d.ts +12 -0
- package/lib/api/index.d.ts.map +1 -0
- package/lib/api/index.js +117 -0
- package/lib/api/index.js.map +1 -0
- package/lib/api/types.d.ts +188 -0
- package/lib/api/types.d.ts.map +1 -0
- package/lib/api/types.js +3 -0
- package/lib/api/types.js.map +1 -0
- package/lib/bridge/js.d.ts +10 -0
- package/lib/bridge/js.d.ts.map +1 -0
- package/lib/bridge/js.js +47 -0
- package/lib/bridge/js.js.map +1 -0
- package/lib/broadcast.d.ts +4 -0
- package/lib/broadcast.d.ts.map +1 -0
- package/lib/broadcast.js +25 -0
- package/lib/broadcast.js.map +1 -0
- package/lib/cli-transaction.d.ts +26 -0
- package/lib/cli-transaction.d.ts.map +1 -0
- package/lib/cli-transaction.js +32 -0
- package/lib/cli-transaction.js.map +1 -0
- package/lib/config.d.ts +9 -0
- package/lib/config.d.ts.map +1 -0
- package/lib/config.js +16 -0
- package/lib/config.js.map +1 -0
- package/lib/createTransaction.d.ts +4 -0
- package/lib/createTransaction.d.ts.map +1 -0
- package/lib/createTransaction.js +18 -0
- package/lib/createTransaction.js.map +1 -0
- package/lib/datasets/dataset-1.d.ts +5 -0
- package/lib/datasets/dataset-1.d.ts.map +1 -0
- package/lib/datasets/dataset-1.js +154 -0
- package/lib/datasets/dataset-1.js.map +1 -0
- package/lib/deviceTransactionConfig.d.ts +11 -0
- package/lib/deviceTransactionConfig.d.ts.map +1 -0
- package/lib/deviceTransactionConfig.js +27 -0
- package/lib/deviceTransactionConfig.js.map +1 -0
- package/lib/estimateMaxSpendable.d.ts +4 -0
- package/lib/estimateMaxSpendable.d.ts.map +1 -0
- package/lib/estimateMaxSpendable.js +31 -0
- package/lib/estimateMaxSpendable.js.map +1 -0
- package/lib/getTransactionStatus.d.ts +4 -0
- package/lib/getTransactionStatus.d.ts.map +1 -0
- package/lib/getTransactionStatus.js +82 -0
- package/lib/getTransactionStatus.js.map +1 -0
- package/lib/hw-getAddress.d.ts +6 -0
- package/lib/hw-getAddress.d.ts.map +1 -0
- package/lib/hw-getAddress.js +23 -0
- package/lib/hw-getAddress.js.map +1 -0
- package/lib/logic.d.ts +11 -0
- package/lib/logic.d.ts.map +1 -0
- package/lib/logic.js +93 -0
- package/lib/logic.js.map +1 -0
- package/lib/prepareTransaction.d.ts +4 -0
- package/lib/prepareTransaction.d.ts.map +1 -0
- package/lib/prepareTransaction.js +53 -0
- package/lib/prepareTransaction.js.map +1 -0
- package/lib/signOperation.d.ts +6 -0
- package/lib/signOperation.d.ts.map +1 -0
- package/lib/signOperation.js +99 -0
- package/lib/signOperation.js.map +1 -0
- package/lib/signer.d.ts +11 -0
- package/lib/signer.d.ts.map +1 -0
- package/lib/signer.js +3 -0
- package/lib/signer.js.map +1 -0
- package/lib/specs.d.ts +7 -0
- package/lib/specs.d.ts.map +1 -0
- package/lib/specs.js +66 -0
- package/lib/specs.js.map +1 -0
- package/lib/speculos-deviceActions.d.ts +4 -0
- package/lib/speculos-deviceActions.d.ts.map +1 -0
- package/lib/speculos-deviceActions.js +49 -0
- package/lib/speculos-deviceActions.js.map +1 -0
- package/lib/synchronization.d.ts +3 -0
- package/lib/synchronization.d.ts.map +1 -0
- package/lib/synchronization.js +74 -0
- package/lib/synchronization.js.map +1 -0
- package/lib/transaction.d.ts +15 -0
- package/lib/transaction.d.ts.map +1 -0
- package/lib/transaction.js +56 -0
- package/lib/transaction.js.map +1 -0
- package/lib/types.d.ts +30 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -0
- package/lib-es/api/index.d.ts +12 -0
- package/lib-es/api/index.d.ts.map +1 -0
- package/lib-es/api/index.js +105 -0
- package/lib-es/api/index.js.map +1 -0
- package/lib-es/api/types.d.ts +188 -0
- package/lib-es/api/types.d.ts.map +1 -0
- package/lib-es/api/types.js +2 -0
- package/lib-es/api/types.js.map +1 -0
- package/lib-es/bridge/js.d.ts +10 -0
- package/lib-es/bridge/js.d.ts.map +1 -0
- package/lib-es/bridge/js.js +40 -0
- package/lib-es/bridge/js.js.map +1 -0
- package/lib-es/broadcast.d.ts +4 -0
- package/lib-es/broadcast.d.ts.map +1 -0
- package/lib-es/broadcast.js +21 -0
- package/lib-es/broadcast.js.map +1 -0
- package/lib-es/cli-transaction.d.ts +26 -0
- package/lib-es/cli-transaction.d.ts.map +1 -0
- package/lib-es/cli-transaction.js +26 -0
- package/lib-es/cli-transaction.js.map +1 -0
- package/lib-es/config.d.ts +9 -0
- package/lib-es/config.d.ts.map +1 -0
- package/lib-es/config.js +11 -0
- package/lib-es/config.js.map +1 -0
- package/lib-es/createTransaction.d.ts +4 -0
- package/lib-es/createTransaction.d.ts.map +1 -0
- package/lib-es/createTransaction.js +11 -0
- package/lib-es/createTransaction.js.map +1 -0
- package/lib-es/datasets/dataset-1.d.ts +5 -0
- package/lib-es/datasets/dataset-1.d.ts.map +1 -0
- package/lib-es/datasets/dataset-1.js +148 -0
- package/lib-es/datasets/dataset-1.js.map +1 -0
- package/lib-es/deviceTransactionConfig.d.ts +11 -0
- package/lib-es/deviceTransactionConfig.d.ts.map +1 -0
- package/lib-es/deviceTransactionConfig.js +25 -0
- package/lib-es/deviceTransactionConfig.js.map +1 -0
- package/lib-es/estimateMaxSpendable.d.ts +4 -0
- package/lib-es/estimateMaxSpendable.d.ts.map +1 -0
- package/lib-es/estimateMaxSpendable.js +24 -0
- package/lib-es/estimateMaxSpendable.js.map +1 -0
- package/lib-es/getTransactionStatus.d.ts +4 -0
- package/lib-es/getTransactionStatus.d.ts.map +1 -0
- package/lib-es/getTransactionStatus.js +75 -0
- package/lib-es/getTransactionStatus.js.map +1 -0
- package/lib-es/hw-getAddress.d.ts +6 -0
- package/lib-es/hw-getAddress.d.ts.map +1 -0
- package/lib-es/hw-getAddress.js +21 -0
- package/lib-es/hw-getAddress.js.map +1 -0
- package/lib-es/logic.d.ts +11 -0
- package/lib-es/logic.d.ts.map +1 -0
- package/lib-es/logic.js +82 -0
- package/lib-es/logic.js.map +1 -0
- package/lib-es/prepareTransaction.d.ts +4 -0
- package/lib-es/prepareTransaction.d.ts.map +1 -0
- package/lib-es/prepareTransaction.js +46 -0
- package/lib-es/prepareTransaction.js.map +1 -0
- package/lib-es/signOperation.d.ts +6 -0
- package/lib-es/signOperation.d.ts.map +1 -0
- package/lib-es/signOperation.js +92 -0
- package/lib-es/signOperation.js.map +1 -0
- package/lib-es/signer.d.ts +11 -0
- package/lib-es/signer.d.ts.map +1 -0
- package/lib-es/signer.js +2 -0
- package/lib-es/signer.js.map +1 -0
- package/lib-es/specs.d.ts +7 -0
- package/lib-es/specs.d.ts.map +1 -0
- package/lib-es/specs.js +61 -0
- package/lib-es/specs.js.map +1 -0
- package/lib-es/speculos-deviceActions.d.ts +4 -0
- package/lib-es/speculos-deviceActions.d.ts.map +1 -0
- package/lib-es/speculos-deviceActions.js +46 -0
- package/lib-es/speculos-deviceActions.js.map +1 -0
- package/lib-es/synchronization.d.ts +3 -0
- package/lib-es/synchronization.d.ts.map +1 -0
- package/lib-es/synchronization.js +67 -0
- package/lib-es/synchronization.js.map +1 -0
- package/lib-es/transaction.d.ts +15 -0
- package/lib-es/transaction.d.ts.map +1 -0
- package/lib-es/transaction.js +50 -0
- package/lib-es/transaction.js.map +1 -0
- package/lib-es/types.d.ts +30 -0
- package/lib-es/types.d.ts.map +1 -0
- package/lib-es/types.js +2 -0
- package/lib-es/types.js.map +1 -0
- package/package.json +81 -0
- package/please-add-coverage.test.ts +3 -0
- package/src/api/index.ts +138 -0
- package/src/api/types.ts +191 -0
- package/src/bridge/js.ts +56 -0
- package/src/broadcast.ts +20 -0
- package/src/cli-transaction.ts +44 -0
- package/src/config.ts +19 -0
- package/src/createTransaction.ts +13 -0
- package/src/datasets/dataset-1.ts +153 -0
- package/src/deviceTransactionConfig.ts +41 -0
- package/src/estimateMaxSpendable.ts +25 -0
- package/src/getTransactionStatus.ts +86 -0
- package/src/hw-getAddress.ts +20 -0
- package/src/logic.ts +101 -0
- package/src/prepareTransaction.ts +48 -0
- package/src/signOperation.ts +129 -0
- package/src/signer.ts +17 -0
- package/src/specs.ts +73 -0
- package/src/speculos-deviceActions.ts +53 -0
- package/src/synchronization.ts +73 -0
- package/src/transaction.ts +79 -0
- package/src/types.ts +39 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AmountRequired,
|
|
3
|
+
FeeNotLoaded,
|
|
4
|
+
FeeRequired,
|
|
5
|
+
FeeTooHigh,
|
|
6
|
+
InvalidAddress,
|
|
7
|
+
InvalidAddressBecauseDestinationIsAlsoSource,
|
|
8
|
+
NotEnoughBalanceBecauseDestinationNotCreated,
|
|
9
|
+
NotEnoughSpendableBalance,
|
|
10
|
+
RecipientRequired,
|
|
11
|
+
} from "@ledgerhq/errors";
|
|
12
|
+
import BigNumber from "bignumber.js";
|
|
13
|
+
import { isValidClassicAddress } from "ripple-address-codec";
|
|
14
|
+
import { Account, AccountBridge } from "@ledgerhq/types-live";
|
|
15
|
+
import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index";
|
|
16
|
+
import { Transaction, TransactionStatus } from "./types";
|
|
17
|
+
import { getServerInfos, parseAPIValue } from "./api";
|
|
18
|
+
import { cachedRecipientIsNew } from "./logic";
|
|
19
|
+
|
|
20
|
+
export const getTransactionStatus: AccountBridge<
|
|
21
|
+
Transaction,
|
|
22
|
+
Account,
|
|
23
|
+
TransactionStatus
|
|
24
|
+
>["getTransactionStatus"] = async (account, transaction) => {
|
|
25
|
+
const errors: Record<string, Error> = {};
|
|
26
|
+
const warnings: Record<string, Error> = {};
|
|
27
|
+
const serverInfos = await getServerInfos();
|
|
28
|
+
const reserveBaseXRP = parseAPIValue(
|
|
29
|
+
serverInfos.info.validated_ledger.reserve_base_xrp.toString(),
|
|
30
|
+
);
|
|
31
|
+
const estimatedFees = new BigNumber(transaction.fee || 0);
|
|
32
|
+
const totalSpent = new BigNumber(transaction.amount).plus(estimatedFees);
|
|
33
|
+
const amount = new BigNumber(transaction.amount);
|
|
34
|
+
|
|
35
|
+
if (amount.gt(0) && estimatedFees.times(10).gt(amount)) {
|
|
36
|
+
warnings.feeTooHigh = new FeeTooHigh();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!transaction.fee) {
|
|
40
|
+
errors.fee = new FeeNotLoaded();
|
|
41
|
+
} else if (transaction.fee.eq(0)) {
|
|
42
|
+
errors.fee = new FeeRequired();
|
|
43
|
+
} else if (totalSpent.gt(account.balance.minus(reserveBaseXRP))) {
|
|
44
|
+
errors.amount = new NotEnoughSpendableBalance("", {
|
|
45
|
+
minimumAmount: formatCurrencyUnit(account.currency.units[0], reserveBaseXRP, {
|
|
46
|
+
disableRounding: true,
|
|
47
|
+
useGrouping: false,
|
|
48
|
+
showCode: true,
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
} else if (
|
|
52
|
+
transaction.recipient &&
|
|
53
|
+
(await cachedRecipientIsNew(transaction.recipient)) &&
|
|
54
|
+
transaction.amount.lt(reserveBaseXRP)
|
|
55
|
+
) {
|
|
56
|
+
errors.amount = new NotEnoughBalanceBecauseDestinationNotCreated("", {
|
|
57
|
+
minimalAmount: formatCurrencyUnit(account.currency.units[0], reserveBaseXRP, {
|
|
58
|
+
disableRounding: true,
|
|
59
|
+
useGrouping: false,
|
|
60
|
+
showCode: true,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!transaction.recipient) {
|
|
66
|
+
errors.recipient = new RecipientRequired("");
|
|
67
|
+
} else if (account.freshAddress === transaction.recipient) {
|
|
68
|
+
errors.recipient = new InvalidAddressBecauseDestinationIsAlsoSource();
|
|
69
|
+
} else if (!isValidClassicAddress(transaction.recipient)) {
|
|
70
|
+
errors.recipient = new InvalidAddress("", {
|
|
71
|
+
currencyName: account.currency.name,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!errors.amount && amount.eq(0)) {
|
|
76
|
+
errors.amount = new AmountRequired();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
errors,
|
|
81
|
+
warnings,
|
|
82
|
+
estimatedFees,
|
|
83
|
+
amount,
|
|
84
|
+
totalSpent,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { GetAddressOptions } from "@ledgerhq/coin-framework/derivation";
|
|
2
|
+
import { GetAddressFn } from "@ledgerhq/coin-framework/bridge/getAddressWrapper";
|
|
3
|
+
import { SignerContext } from "@ledgerhq/coin-framework/signer";
|
|
4
|
+
import { XrpAddress, XrpSigner } from "./signer";
|
|
5
|
+
|
|
6
|
+
const resolver = (signerContext: SignerContext<XrpSigner>): GetAddressFn => {
|
|
7
|
+
return async (deviceId: string, { path, verify }: GetAddressOptions) => {
|
|
8
|
+
const { address, publicKey } = (await signerContext(deviceId, signer =>
|
|
9
|
+
signer.getAddress(path, verify, false),
|
|
10
|
+
)) as XrpAddress;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
path,
|
|
14
|
+
address,
|
|
15
|
+
publicKey,
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default resolver;
|
package/src/logic.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
import { Account, Operation } from "@ledgerhq/types-live";
|
|
3
|
+
import { isValidClassicAddress } from "ripple-address-codec";
|
|
4
|
+
import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
|
|
5
|
+
import { XrplOperation } from "./api/types";
|
|
6
|
+
import { getAccountInfo } from "./api";
|
|
7
|
+
|
|
8
|
+
export const NEW_ACCOUNT_ERROR_MESSAGE = "actNotFound";
|
|
9
|
+
export const UINT32_MAX = new BigNumber(2).pow(32).minus(1);
|
|
10
|
+
|
|
11
|
+
/** @see https://xrpl.org/basic-data-types.html#specifying-time */
|
|
12
|
+
const RIPPLE_EPOCH = 946684800;
|
|
13
|
+
|
|
14
|
+
export const validateTag = (tag: BigNumber) => {
|
|
15
|
+
return (
|
|
16
|
+
!tag.isNaN() && tag.isFinite() && tag.isInteger() && tag.isPositive() && tag.lte(UINT32_MAX)
|
|
17
|
+
);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const getNextValidSequence = async (account: Account) => {
|
|
21
|
+
const accInfo = await getAccountInfo(account.freshAddress, true);
|
|
22
|
+
return accInfo.account_data.Sequence;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function isRecipientValid(recipient: string): boolean {
|
|
26
|
+
return isValidClassicAddress(recipient);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const recipientIsNew = async (recipient: string): Promise<boolean> => {
|
|
30
|
+
if (!isRecipientValid(recipient)) return false;
|
|
31
|
+
|
|
32
|
+
const info = await getAccountInfo(recipient);
|
|
33
|
+
if (info.error === NEW_ACCOUNT_ERROR_MESSAGE) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const cacheRecipientsNew: Record<string, boolean> = {};
|
|
40
|
+
export const cachedRecipientIsNew = async (recipient: string) => {
|
|
41
|
+
if (recipient in cacheRecipientsNew) return cacheRecipientsNew[recipient];
|
|
42
|
+
cacheRecipientsNew[recipient] = await recipientIsNew(recipient);
|
|
43
|
+
return cacheRecipientsNew[recipient];
|
|
44
|
+
};
|
|
45
|
+
export const removeCachedRecipientIsNew = (recipient: string) => {
|
|
46
|
+
delete cacheRecipientsNew[recipient];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const XrplOperationToLiveOperation =
|
|
50
|
+
(accountId: string, address: string) =>
|
|
51
|
+
({
|
|
52
|
+
meta: { delivered_amount },
|
|
53
|
+
tx: { Fee, hash, inLedger, date, Account, Destination, Sequence },
|
|
54
|
+
}: XrplOperation) => {
|
|
55
|
+
const type = Account === address ? "OUT" : "IN";
|
|
56
|
+
let value =
|
|
57
|
+
delivered_amount && typeof delivered_amount === "string"
|
|
58
|
+
? new BigNumber(delivered_amount)
|
|
59
|
+
: new BigNumber(0);
|
|
60
|
+
const feeValue = new BigNumber(Fee);
|
|
61
|
+
|
|
62
|
+
if (type === "OUT") {
|
|
63
|
+
if (!Number.isNaN(feeValue)) {
|
|
64
|
+
value = value.plus(feeValue);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const toEpochDate = (RIPPLE_EPOCH + date) * 1000;
|
|
69
|
+
|
|
70
|
+
const op: Operation = {
|
|
71
|
+
id: encodeOperationId(accountId, hash, type),
|
|
72
|
+
hash: hash,
|
|
73
|
+
accountId: accountId,
|
|
74
|
+
type,
|
|
75
|
+
value,
|
|
76
|
+
fee: feeValue,
|
|
77
|
+
blockHash: null,
|
|
78
|
+
blockHeight: inLedger,
|
|
79
|
+
senders: [Account],
|
|
80
|
+
recipients: [Destination],
|
|
81
|
+
date: new Date(toEpochDate),
|
|
82
|
+
transactionSequenceNumber: Sequence,
|
|
83
|
+
extra: {},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return op;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const filterOperations = (
|
|
90
|
+
transactions: XrplOperation[],
|
|
91
|
+
accountId: string,
|
|
92
|
+
address: string,
|
|
93
|
+
) => {
|
|
94
|
+
return transactions
|
|
95
|
+
.filter(
|
|
96
|
+
({ tx, meta }: XrplOperation) =>
|
|
97
|
+
tx.TransactionType === "Payment" && typeof meta.delivered_amount === "string",
|
|
98
|
+
)
|
|
99
|
+
.map(XrplOperationToLiveOperation(accountId, address))
|
|
100
|
+
.filter((op): op is Operation => Boolean(op));
|
|
101
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
import { NetworkDown } from "@ledgerhq/errors";
|
|
3
|
+
import { AccountBridge } from "@ledgerhq/types-live";
|
|
4
|
+
import { getServerInfos, parseAPIValue } from "./api";
|
|
5
|
+
import { NetworkInfo, Transaction } from "./types";
|
|
6
|
+
|
|
7
|
+
// FIXME this could be cleaner
|
|
8
|
+
const remapError = (error: Error) => {
|
|
9
|
+
const msg = error.message;
|
|
10
|
+
|
|
11
|
+
if (msg.includes("Unable to resolve host") || msg.includes("Network is down")) {
|
|
12
|
+
return new NetworkDown();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return error;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const prepareTransaction: AccountBridge<Transaction>["prepareTransaction"] = async (
|
|
19
|
+
account,
|
|
20
|
+
transaction,
|
|
21
|
+
) => {
|
|
22
|
+
let networkInfo: NetworkInfo | null | undefined = transaction.networkInfo;
|
|
23
|
+
|
|
24
|
+
if (!networkInfo) {
|
|
25
|
+
try {
|
|
26
|
+
const info = await getServerInfos();
|
|
27
|
+
const serverFee = parseAPIValue(info.info.validated_ledger.base_fee_xrp.toString());
|
|
28
|
+
networkInfo = {
|
|
29
|
+
family: "xrp",
|
|
30
|
+
serverFee,
|
|
31
|
+
baseReserve: new BigNumber(0), // NOT USED. will refactor later.
|
|
32
|
+
};
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if (e instanceof Error) {
|
|
35
|
+
throw remapError(e);
|
|
36
|
+
}
|
|
37
|
+
throw e;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const fee = transaction.fee || networkInfo.serverFee;
|
|
42
|
+
|
|
43
|
+
if (transaction.networkInfo !== networkInfo || transaction.fee !== fee) {
|
|
44
|
+
return { ...transaction, networkInfo, fee };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return transaction;
|
|
48
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import invariant from "invariant";
|
|
2
|
+
import { Observable } from "rxjs";
|
|
3
|
+
import BigNumber from "bignumber.js";
|
|
4
|
+
import { encode } from "ripple-binary-codec";
|
|
5
|
+
import { FeeNotLoaded } from "@ledgerhq/errors";
|
|
6
|
+
import { AccountBridge, Operation } from "@ledgerhq/types-live";
|
|
7
|
+
import { SignerContext } from "@ledgerhq/coin-framework/signer";
|
|
8
|
+
import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
|
|
9
|
+
import XrplDefinitions from "ripple-binary-codec/dist/enums/definitions.json";
|
|
10
|
+
import { getNextValidSequence, removeCachedRecipientIsNew, UINT32_MAX, validateTag } from "./logic";
|
|
11
|
+
import { XrpSignature, XrpSigner } from "./signer";
|
|
12
|
+
import { getLedgerIndex } from "./api";
|
|
13
|
+
import { Transaction } from "./types";
|
|
14
|
+
|
|
15
|
+
const LEDGER_OFFSET = 20;
|
|
16
|
+
|
|
17
|
+
const { TRANSACTION_TYPES } = XrplDefinitions;
|
|
18
|
+
type XrplTransaction = {
|
|
19
|
+
TransactionType: keyof typeof TRANSACTION_TYPES;
|
|
20
|
+
Flags: number;
|
|
21
|
+
Account: string;
|
|
22
|
+
Amount: string;
|
|
23
|
+
Destination: string;
|
|
24
|
+
DestinationTag: number | undefined;
|
|
25
|
+
Fee: string;
|
|
26
|
+
Sequence: number;
|
|
27
|
+
LastLedgerSequence: number;
|
|
28
|
+
SigningPubKey?: string;
|
|
29
|
+
TxnSignature?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const buildSignOperation =
|
|
33
|
+
(signerContext: SignerContext<XrpSigner>): AccountBridge<Transaction>["signOperation"] =>
|
|
34
|
+
({ account, deviceId, transaction }) =>
|
|
35
|
+
new Observable(o => {
|
|
36
|
+
removeCachedRecipientIsNew(transaction.recipient);
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
const { fee } = transaction;
|
|
40
|
+
if (!fee) throw new FeeNotLoaded();
|
|
41
|
+
try {
|
|
42
|
+
const tag = transaction.tag ? transaction.tag : undefined;
|
|
43
|
+
const nextSequenceNumber = await getNextValidSequence(account);
|
|
44
|
+
const xrplTransaction: XrplTransaction = {
|
|
45
|
+
TransactionType: "Payment",
|
|
46
|
+
Account: account.freshAddress,
|
|
47
|
+
Amount: transaction.amount.toFixed(),
|
|
48
|
+
Destination: transaction.recipient,
|
|
49
|
+
DestinationTag: tag,
|
|
50
|
+
Fee: fee.toFixed(),
|
|
51
|
+
Flags: 2147483648,
|
|
52
|
+
Sequence: nextSequenceNumber,
|
|
53
|
+
LastLedgerSequence: (await getLedgerIndex()) + LEDGER_OFFSET,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (tag)
|
|
57
|
+
invariant(
|
|
58
|
+
validateTag(new BigNumber(tag)),
|
|
59
|
+
`tag is set but is not in a valid format, should be between [0 - ${UINT32_MAX.toString()}]`,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
o.next({
|
|
63
|
+
type: "device-signature-requested",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const signature = (await signerContext(deviceId, async signer => {
|
|
67
|
+
const { freshAddressPath: derivationPath } = account;
|
|
68
|
+
const { publicKey } = await signer.getAddress(derivationPath);
|
|
69
|
+
|
|
70
|
+
const serializedTransaction = encode({
|
|
71
|
+
...xrplTransaction,
|
|
72
|
+
SigningPubKey: publicKey,
|
|
73
|
+
});
|
|
74
|
+
const transactionSignature = await signer.signTransaction(
|
|
75
|
+
derivationPath,
|
|
76
|
+
serializedTransaction,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return encode({
|
|
80
|
+
...xrplTransaction,
|
|
81
|
+
SigningPubKey: publicKey,
|
|
82
|
+
TxnSignature: transactionSignature,
|
|
83
|
+
}).toUpperCase();
|
|
84
|
+
})) as XrpSignature;
|
|
85
|
+
|
|
86
|
+
o.next({
|
|
87
|
+
type: "device-signature-granted",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const hash = "";
|
|
91
|
+
const operation: Operation = {
|
|
92
|
+
id: encodeOperationId(account.id, hash, "OUT"),
|
|
93
|
+
hash,
|
|
94
|
+
accountId: account.id,
|
|
95
|
+
type: "OUT",
|
|
96
|
+
value: transaction.amount,
|
|
97
|
+
fee,
|
|
98
|
+
blockHash: null,
|
|
99
|
+
blockHeight: null,
|
|
100
|
+
senders: [account.freshAddress],
|
|
101
|
+
recipients: [transaction.recipient],
|
|
102
|
+
date: new Date(),
|
|
103
|
+
transactionSequenceNumber: nextSequenceNumber,
|
|
104
|
+
extra: {},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
o.next({
|
|
108
|
+
type: "signed",
|
|
109
|
+
signedOperation: {
|
|
110
|
+
operation,
|
|
111
|
+
signature,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
} catch (e) {
|
|
115
|
+
if (e instanceof Error) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
(e as Error & { data?: { resultMessage?: string } })?.data?.resultMessage,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw e;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
main().then(
|
|
126
|
+
() => o.complete(),
|
|
127
|
+
e => o.error(e),
|
|
128
|
+
);
|
|
129
|
+
});
|
package/src/signer.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type XrpAddress = {
|
|
2
|
+
publicKey: string;
|
|
3
|
+
address: string;
|
|
4
|
+
chainCode?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type XrpSignature = string; // `0x${string}`
|
|
8
|
+
|
|
9
|
+
export interface XrpSigner {
|
|
10
|
+
getAddress(
|
|
11
|
+
path: string,
|
|
12
|
+
display?: boolean,
|
|
13
|
+
chainCode?: boolean,
|
|
14
|
+
ed25519?: boolean,
|
|
15
|
+
): Promise<XrpAddress>;
|
|
16
|
+
signTransaction(path: string, rawTxHex: string, ed25519?: boolean): Promise<XrpSignature>;
|
|
17
|
+
}
|
package/src/specs.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import invariant from "invariant";
|
|
2
|
+
import expect from "expect";
|
|
3
|
+
import { DeviceModelId } from "@ledgerhq/devices";
|
|
4
|
+
import type { AppSpec } from "@ledgerhq/coin-framework/bot/types";
|
|
5
|
+
import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies";
|
|
6
|
+
import { parseCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index";
|
|
7
|
+
import { botTest, genericTestDestination, pickSiblings } from "@ledgerhq/coin-framework/bot/specs";
|
|
8
|
+
import { acceptTransaction } from "./speculos-deviceActions";
|
|
9
|
+
import type { Transaction } from "./types";
|
|
10
|
+
|
|
11
|
+
const currency = getCryptoCurrencyById("ripple");
|
|
12
|
+
const minAmountCutoff = parseCurrencyUnit(currency.units[0], "0.1");
|
|
13
|
+
const reserve = parseCurrencyUnit(currency.units[0], "20");
|
|
14
|
+
|
|
15
|
+
const xrp: AppSpec<Transaction> = {
|
|
16
|
+
name: "XRP",
|
|
17
|
+
currency,
|
|
18
|
+
appQuery: {
|
|
19
|
+
model: DeviceModelId.nanoS,
|
|
20
|
+
appName: "XRP",
|
|
21
|
+
},
|
|
22
|
+
genericDeviceAction: acceptTransaction,
|
|
23
|
+
minViableAmount: minAmountCutoff,
|
|
24
|
+
mutations: [
|
|
25
|
+
{
|
|
26
|
+
name: "move ~50%",
|
|
27
|
+
maxRun: 2,
|
|
28
|
+
testDestination: genericTestDestination,
|
|
29
|
+
transaction: ({ account, siblings, bridge, maxSpendable }) => {
|
|
30
|
+
invariant(maxSpendable.gt(minAmountCutoff), "balance is too low");
|
|
31
|
+
const transaction = bridge.createTransaction(account);
|
|
32
|
+
const sibling = pickSiblings(siblings, 3);
|
|
33
|
+
const recipient = sibling.freshAddress;
|
|
34
|
+
let amount = maxSpendable.div(1.9 + 0.2 * Math.random()).integerValue();
|
|
35
|
+
|
|
36
|
+
if (!sibling.used && amount.lt(reserve)) {
|
|
37
|
+
invariant(
|
|
38
|
+
maxSpendable.gt(reserve.plus(minAmountCutoff)),
|
|
39
|
+
"not enough funds to send to new account",
|
|
40
|
+
);
|
|
41
|
+
amount = reserve;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
transaction,
|
|
46
|
+
updates: [
|
|
47
|
+
{
|
|
48
|
+
amount,
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
recipient,
|
|
52
|
+
},
|
|
53
|
+
Math.random() > 0.5
|
|
54
|
+
? {
|
|
55
|
+
tag: 123,
|
|
56
|
+
}
|
|
57
|
+
: null,
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
test: ({ account, accountBeforeTransaction, operation }) => {
|
|
62
|
+
botTest("account balance moved with operation.value", () =>
|
|
63
|
+
expect(account.balance.toString()).toBe(
|
|
64
|
+
accountBeforeTransaction.balance.minus(operation.value).toString(),
|
|
65
|
+
),
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
export default {
|
|
72
|
+
xrp,
|
|
73
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { DeviceAction } from "@ledgerhq/coin-framework/bot/types";
|
|
2
|
+
import type { Transaction } from "./types";
|
|
3
|
+
import {
|
|
4
|
+
deviceActionFlow,
|
|
5
|
+
formatDeviceAmount,
|
|
6
|
+
SpeculosButton,
|
|
7
|
+
} from "@ledgerhq/coin-framework/bot/specs";
|
|
8
|
+
|
|
9
|
+
export const acceptTransaction: DeviceAction<Transaction, any> = deviceActionFlow({
|
|
10
|
+
steps: [
|
|
11
|
+
{
|
|
12
|
+
title: "Transaction Type",
|
|
13
|
+
button: SpeculosButton.RIGHT,
|
|
14
|
+
expectedValue: () => "Payment",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: "Amount",
|
|
18
|
+
button: SpeculosButton.RIGHT,
|
|
19
|
+
expectedValue: ({ account, status }) => formatDeviceAmount(account.currency, status.amount),
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
title: "Fee",
|
|
23
|
+
button: SpeculosButton.RIGHT,
|
|
24
|
+
expectedValue: ({ account, status }) =>
|
|
25
|
+
formatDeviceAmount(account.currency, status.estimatedFees),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
title: "Destination Tag",
|
|
29
|
+
button: SpeculosButton.RIGHT,
|
|
30
|
+
expectedValue: ({ transaction }) => String(transaction.tag),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
title: "Destination",
|
|
34
|
+
button: SpeculosButton.RIGHT,
|
|
35
|
+
trimValue: true,
|
|
36
|
+
expectedValue: ({ transaction }) => transaction.recipient,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
title: "Account",
|
|
40
|
+
button: SpeculosButton.RIGHT,
|
|
41
|
+
trimValue: true,
|
|
42
|
+
expectedValue: ({ account }) => account.freshAddress,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: "Accept",
|
|
46
|
+
button: SpeculosButton.BOTH,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: "Sign transaction",
|
|
50
|
+
button: SpeculosButton.BOTH,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import BigNumber from "bignumber.js";
|
|
2
|
+
import { Operation } from "@ledgerhq/types-live";
|
|
3
|
+
import { encodeAccountId } from "@ledgerhq/coin-framework/account/index";
|
|
4
|
+
import { GetAccountShape, mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers";
|
|
5
|
+
import { getAccountInfo, getServerInfos, getTransactions, parseAPIValue } from "./api";
|
|
6
|
+
import { NEW_ACCOUNT_ERROR_MESSAGE, filterOperations } from "./logic";
|
|
7
|
+
|
|
8
|
+
export const getAccountShape: GetAccountShape = async info => {
|
|
9
|
+
const { address, initialAccount, currency, derivationMode } = info;
|
|
10
|
+
const accountId = encodeAccountId({
|
|
11
|
+
type: "js",
|
|
12
|
+
version: "2",
|
|
13
|
+
currencyId: currency.id,
|
|
14
|
+
xpubOrAddress: address,
|
|
15
|
+
derivationMode,
|
|
16
|
+
});
|
|
17
|
+
const accountInfo = await getAccountInfo(address);
|
|
18
|
+
|
|
19
|
+
if (!accountInfo || accountInfo.error === NEW_ACCOUNT_ERROR_MESSAGE) {
|
|
20
|
+
return {
|
|
21
|
+
id: accountId,
|
|
22
|
+
xpub: address,
|
|
23
|
+
blockHeight: 0,
|
|
24
|
+
balance: new BigNumber(0),
|
|
25
|
+
spendableBalance: new BigNumber(0),
|
|
26
|
+
operations: [],
|
|
27
|
+
operationsCount: 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const serverInfo = await getServerInfos();
|
|
32
|
+
const reserveMinXRP = parseAPIValue(serverInfo.info.validated_ledger.reserve_base_xrp.toString());
|
|
33
|
+
const reservePerTrustline = parseAPIValue(
|
|
34
|
+
serverInfo.info.validated_ledger.reserve_inc_xrp.toString(),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const oldOperations = initialAccount?.operations || [];
|
|
38
|
+
const startAt = oldOperations.length ? (oldOperations[0].blockHeight || 0) + 1 : 0;
|
|
39
|
+
|
|
40
|
+
const ledgers = serverInfo.info.complete_ledgers.split("-");
|
|
41
|
+
const minLedgerVersion = Number(ledgers[0]);
|
|
42
|
+
const maxLedgerVersion = Number(ledgers[1]);
|
|
43
|
+
|
|
44
|
+
const trustlines = accountInfo.account_data.OwnerCount;
|
|
45
|
+
|
|
46
|
+
const balance = new BigNumber(accountInfo.account_data.Balance);
|
|
47
|
+
const spendableBalance = new BigNumber(accountInfo.account_data.Balance)
|
|
48
|
+
.minus(reserveMinXRP)
|
|
49
|
+
.minus(reservePerTrustline.times(trustlines));
|
|
50
|
+
|
|
51
|
+
const newTransactions = await getTransactions(address, {
|
|
52
|
+
ledger_index_min: Math.max(
|
|
53
|
+
startAt, // if there is no ops, it might be after a clear and we prefer to pull from the oldest possible history
|
|
54
|
+
minLedgerVersion,
|
|
55
|
+
),
|
|
56
|
+
ledger_index_max: maxLedgerVersion,
|
|
57
|
+
});
|
|
58
|
+
const newOperations = filterOperations(newTransactions, accountId, address);
|
|
59
|
+
|
|
60
|
+
const operations = mergeOps(oldOperations, newOperations as Operation[]);
|
|
61
|
+
|
|
62
|
+
const shape = {
|
|
63
|
+
id: accountId,
|
|
64
|
+
xpub: address,
|
|
65
|
+
blockHeight: maxLedgerVersion,
|
|
66
|
+
balance,
|
|
67
|
+
spendableBalance,
|
|
68
|
+
operations,
|
|
69
|
+
operationsCount: operations.length,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return shape;
|
|
73
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { BigNumber } from "bignumber.js";
|
|
2
|
+
import type { Transaction, TransactionRaw } from "./types";
|
|
3
|
+
import { formatTransactionStatus } from "@ledgerhq/coin-framework/formatters";
|
|
4
|
+
import {
|
|
5
|
+
fromTransactionCommonRaw,
|
|
6
|
+
fromTransactionStatusRawCommon as fromTransactionStatusRaw,
|
|
7
|
+
toTransactionCommonRaw,
|
|
8
|
+
toTransactionStatusRawCommon as toTransactionStatusRaw,
|
|
9
|
+
} from "@ledgerhq/coin-framework/serialization/transaction";
|
|
10
|
+
import type { Account } from "@ledgerhq/types-live";
|
|
11
|
+
import { getAccountCurrency } from "@ledgerhq/coin-framework/account/index";
|
|
12
|
+
import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index";
|
|
13
|
+
|
|
14
|
+
export const formatTransaction = (
|
|
15
|
+
{ amount, recipient, fee, tag, useAllAmount }: Transaction,
|
|
16
|
+
account: Account,
|
|
17
|
+
): string => `
|
|
18
|
+
SEND ${
|
|
19
|
+
useAllAmount
|
|
20
|
+
? "MAX"
|
|
21
|
+
: formatCurrencyUnit(getAccountCurrency(account).units[0], amount, {
|
|
22
|
+
showCode: true,
|
|
23
|
+
disableRounding: true,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
TO ${recipient}
|
|
27
|
+
with fee=${
|
|
28
|
+
!fee
|
|
29
|
+
? "?"
|
|
30
|
+
: formatCurrencyUnit(getAccountCurrency(account).units[0], fee, {
|
|
31
|
+
showCode: true,
|
|
32
|
+
disableRounding: true,
|
|
33
|
+
})
|
|
34
|
+
}${tag ? "\n tag=" + tag : ""}`;
|
|
35
|
+
|
|
36
|
+
export const fromTransactionRaw = (tr: TransactionRaw): Transaction => {
|
|
37
|
+
const common = fromTransactionCommonRaw(tr);
|
|
38
|
+
const { networkInfo } = tr;
|
|
39
|
+
return {
|
|
40
|
+
...common,
|
|
41
|
+
family: tr.family,
|
|
42
|
+
tag: tr.tag,
|
|
43
|
+
fee: tr.fee ? new BigNumber(tr.fee) : null,
|
|
44
|
+
feeCustomUnit: tr.feeCustomUnit,
|
|
45
|
+
// FIXME remove this field. this is not good.. we're dereferencing here. we should instead store an index (to lookup in currency.units on UI)
|
|
46
|
+
networkInfo: networkInfo && {
|
|
47
|
+
family: networkInfo.family,
|
|
48
|
+
serverFee: new BigNumber(networkInfo.serverFee),
|
|
49
|
+
baseReserve: new BigNumber(networkInfo.baseReserve),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const toTransactionRaw = (t: Transaction): TransactionRaw => {
|
|
55
|
+
const common = toTransactionCommonRaw(t);
|
|
56
|
+
const { networkInfo } = t;
|
|
57
|
+
return {
|
|
58
|
+
...common,
|
|
59
|
+
family: t.family,
|
|
60
|
+
tag: t.tag,
|
|
61
|
+
fee: t.fee ? t.fee.toString() : null,
|
|
62
|
+
feeCustomUnit: t.feeCustomUnit,
|
|
63
|
+
// FIXME remove this field. this is not good.. we're dereferencing here. we should instead store an index (to lookup in currency.units on UI)
|
|
64
|
+
networkInfo: networkInfo && {
|
|
65
|
+
family: networkInfo.family,
|
|
66
|
+
serverFee: networkInfo.serverFee.toString(),
|
|
67
|
+
baseReserve: networkInfo.baseReserve.toString(),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default {
|
|
73
|
+
formatTransaction,
|
|
74
|
+
fromTransactionRaw,
|
|
75
|
+
toTransactionRaw,
|
|
76
|
+
fromTransactionStatusRaw,
|
|
77
|
+
toTransactionStatusRaw,
|
|
78
|
+
formatTransactionStatus,
|
|
79
|
+
};
|