Skip to content

Commit 74bf010

Browse files
Merge pull request #14647 from LedgerHQ/feat/aleo-private-balance
feat(LIVE-25440) aleo private balance
2 parents 5d5dcf4 + f3958fa commit 74bf010

File tree

22 files changed

+915
-16
lines changed

22 files changed

+915
-16
lines changed

.changeset/gold-radios-marry.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ledgerhq/coin-aleo": minor
3+
"ledger-live-desktop": minor
4+
---
5+
6+
aleo private balance integration

apps/ledger-live-desktop/src/renderer/families/aleo/AccountBalanceSummaryFooter.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ describe("AccountBalanceSummaryFooter", () => {
2323
aleoResources: {
2424
transparentBalance: mockTransparentBalance,
2525
provableApi: null,
26+
privateBalance: null,
27+
unspentPrivateRecords: [],
28+
lastPrivateSyncDate: new Date(),
2629
},
2730
};
2831

apps/ledger-live-desktop/src/renderer/families/aleo/AccountBalanceSummaryFooter.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const AccountBalanceSummaryFooter = ({ account }: Readonly<Props>) => {
3636

3737
const spendableBalance = account.spendableBalance;
3838
const transparentBalance = account.aleoResources.transparentBalance;
39-
const privateBalance = null; // private sync will be added later
39+
const privateBalance = account.aleoResources.privateBalance;
4040

4141
const formattedAvailableBalance = formatCurrencyUnit(unit, spendableBalance, formatConfig);
4242
const formattedTransparentBalance = formatCurrencyUnit(unit, transparentBalance, formatConfig);

libs/coin-modules/coin-aleo/src/__tests__/fixtures/account.fixture.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,31 @@ const defaultMockAccountId =
88
"js:2:aleo:aleo1zcwqycj02lccfuu57dzjhva7w5dpzc7pngl0sxjhp58t6vlnnqxs6lnp6f::AViewKey123";
99

1010
export const mockAleoResources: AleoResources = {
11-
transparentBalance: new BigNumber(0),
12-
provableApi: null,
11+
transparentBalance: new BigNumber(1000),
12+
provableApi: {
13+
apiKey: "abc",
14+
consumerId: "consumer123",
15+
jwt: {
16+
token: "jwt_token",
17+
exp: Math.floor(Date.now() / 1000) + 3600,
18+
},
19+
uuid: "uuid-1234",
20+
scannerStatus: {
21+
percentage: 50,
22+
synced: false,
23+
},
24+
},
25+
privateBalance: new BigNumber(1),
26+
unspentPrivateRecords: [],
27+
lastPrivateSyncDate: new Date(),
1328
};
1429

1530
export const mockAleoResourcesRaw: AleoResourcesRaw = {
16-
transparentBalance: "0",
17-
provableApi: null,
31+
transparentBalance: mockAleoResources.transparentBalance.toString(),
32+
provableApi: JSON.stringify(mockAleoResources.provableApi),
33+
privateBalance: mockAleoResources.privateBalance?.toString() ?? null,
34+
unspentPrivateRecords: JSON.stringify(mockAleoResources.unspentPrivateRecords),
35+
lastPrivateSyncDate: mockAleoResources.lastPrivateSyncDate?.toISOString() ?? null,
1836
};
1937

2038
export const getMockedAccount = (overrides?: Partial<AleoAccount>): AleoAccount => {

libs/coin-modules/coin-aleo/src/__tests__/fixtures/api.fixture.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { PROGRAM_ID } from "../../constants";
2-
import { EnrichedTransaction } from "../../types";
3-
import type { AleoPublicTransaction, AleoPublicTransactionDetailsResponse } from "../../types/api";
2+
import {
3+
AleoPrivateRecord,
4+
AleoPublicTransaction,
5+
AleoPublicTransactionDetailsResponse,
6+
EnrichedTransaction,
7+
} from "../../types";
48

59
export const getMockedTransaction = (
610
overrides?: Partial<AleoPublicTransaction>,
@@ -63,3 +67,26 @@ export function getMockedEnrichedTransaction(
6367
...overrides,
6468
};
6569
}
70+
71+
export const testnetViewKey = "AViewKey1tTb4WYnMFnDWjSgTSA5VkiyLKNZH1szDcMyEuzSu1zbk";
72+
73+
// this record has `microcredits: "800000u64.private"` in the decrypted data
74+
export const testnetPrivateRecord: AleoPrivateRecord = {
75+
block_height: 14192647,
76+
block_timestamp: 1770127220,
77+
commitment: "5577911026701224136131721605774668283349812508334064746703596134075753528694field",
78+
function_name: "transfer_public_to_private",
79+
output_index: 0,
80+
owner: "4061324383530370528773115724536366126386700749943799382889243452721616108297field",
81+
program_name: "credits.aleo",
82+
record_ciphertext:
83+
"record1qvqsps6wqrka73247spvsvdlgwr8qhmn5f4uze4t8zutp4k8mwm3zdgtqyxx66trwfhkxun9v35hguerqqpqzqrpdge64jwzyz32aknuxc800uugfwv52pqse4dk4p32datlzpd8z95td5t0dhdm4dfhtq9w285uj2arltzky4u6hmdv2xpdnkv365l3qg9hn0g",
84+
record_name: "credits",
85+
sender: "aleo1zcwqycj02lccfuu57dzjhva7w5dpzc7pngl0sxjhp58t6vlnnqxs6lnp6f",
86+
spent: false,
87+
tag: "4138557248634429596246575371443174357174703200753459213664031563822892655489field",
88+
transaction_id: "at144lgzzq73r38hx4jvtecxteg8v32creg2pxpazs9n4xcduu5jsfqv43lx9 ",
89+
transition_id: "au1mxddgfe8yl0yadjrm6qaz6wqljtlqth885muwxvrzvpd9852cyqqxhxr76 ",
90+
transaction_index: 0,
91+
transition_index: 0,
92+
};

libs/coin-modules/coin-aleo/src/bridge/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ import type {
1515
import getAddressWrapper from "@ledgerhq/coin-framework/bridge/getAddressWrapper";
1616
import type { Observable } from "rxjs";
1717
import aleoCoinConfig, { type AleoCoinConfig } from "../config";
18-
import type { Transaction as AleoTransaction } from "../types/index";
18+
import type { AleoAccount, Transaction as AleoTransaction } from "../types/index";
1919
import type { AleoSigner } from "../types/signer";
2020
import resolver from "../signer/getAddress";
2121
import { validateAddress } from "../logic/validateAddress";
2222
import { estimateMaxSpendable } from "./estimateMaxSpendable";
2323
import { getAccountShape, sync } from "./sync";
2424
import { createTransaction } from "./createTransaction";
2525
import { prepareTransaction } from "./prepareTransaction";
26+
import { assignFromAccountRaw, assignToAccountRaw } from "./serialization";
2627
import { getTransactionStatus } from "./getTransactionStatus";
2728

2829
export function buildCurrencyBridge(signerContext: SignerContext<AleoSigner>): CurrencyBridge {
@@ -42,7 +43,7 @@ export function buildCurrencyBridge(signerContext: SignerContext<AleoSigner>): C
4243

4344
export function buildAccountBridge(
4445
signerContext: SignerContext<AleoSigner>,
45-
): AccountBridge<AleoTransaction> {
46+
): AccountBridge<AleoTransaction, AleoAccount> {
4647
const getAddress = resolver(signerContext);
4748
const receive = makeAccountBridgeReceive(getAddressWrapper(getAddress));
4849

@@ -63,6 +64,8 @@ export function buildAccountBridge(
6364
throw new Error("broadcast is not supported");
6465
},
6566
estimateMaxSpendable,
67+
assignFromAccountRaw,
68+
assignToAccountRaw,
6669
getSerializedAddressParameters,
6770
validateAddress,
6871
};
@@ -71,7 +74,7 @@ export function buildAccountBridge(
7174
export function createBridges(
7275
signerContext: SignerContext<AleoSigner>,
7376
coinConfig: CoinConfig<AleoCoinConfig>,
74-
): Bridge<AleoTransaction> {
77+
): Bridge<AleoTransaction, AleoAccount> {
7578
aleoCoinConfig.setCoinConfig(coinConfig);
7679

7780
return {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import {
2+
getMockedAccount,
3+
getMockedAccountRaw,
4+
mockAleoResources,
5+
mockAleoResourcesRaw,
6+
} from "../__tests__/fixtures/account.fixture";
7+
import type { AleoAccount, AleoAccountRaw } from "../types";
8+
import {
9+
assignFromAccountRaw,
10+
assignToAccountRaw,
11+
toAleoResourcesRaw,
12+
fromAleoResourcesRaw,
13+
} from "./serialization";
14+
15+
describe("serialization", () => {
16+
let mockedAccount: AleoAccount;
17+
let mockedAccountRaw: AleoAccountRaw;
18+
19+
beforeEach(() => {
20+
mockedAccount = getMockedAccount();
21+
mockedAccountRaw = getMockedAccountRaw();
22+
});
23+
24+
it("should serialize AleoResources to raw format", () => {
25+
const result = toAleoResourcesRaw(mockAleoResources);
26+
27+
expect(result).toEqual(mockAleoResourcesRaw);
28+
});
29+
30+
it("should deserialize raw format back to AleoResources", () => {
31+
const result = fromAleoResourcesRaw(mockAleoResourcesRaw);
32+
33+
expect(result).toEqual(mockAleoResources);
34+
});
35+
36+
it("should write serialized resources onto AccountRaw", () => {
37+
assignToAccountRaw(mockedAccount, mockedAccountRaw);
38+
39+
expect(mockedAccountRaw.aleoResources).toEqual(mockAleoResourcesRaw);
40+
});
41+
42+
it("should read and deserialize resources from AccountRaw onto Account", () => {
43+
assignFromAccountRaw(mockedAccountRaw, mockedAccount);
44+
45+
expect(mockedAccount.aleoResources).toEqual(mockAleoResources);
46+
});
47+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import BigNumber from "bignumber.js";
2+
import type { AccountRaw, Account } from "@ledgerhq/types-live";
3+
import type { AleoAccount, AleoAccountRaw, AleoResources, AleoResourcesRaw } from "../types";
4+
5+
export function toAleoResourcesRaw(resources: AleoResources): AleoResourcesRaw {
6+
return {
7+
transparentBalance: resources.transparentBalance.toString(),
8+
privateBalance: resources.privateBalance?.toString() ?? null,
9+
provableApi: resources.provableApi ? JSON.stringify(resources.provableApi) : null,
10+
lastPrivateSyncDate: resources.lastPrivateSyncDate
11+
? resources.lastPrivateSyncDate.toISOString()
12+
: null,
13+
unspentPrivateRecords: resources.unspentPrivateRecords
14+
? JSON.stringify(resources.unspentPrivateRecords)
15+
: null,
16+
};
17+
}
18+
19+
export function fromAleoResourcesRaw(rawResources: AleoResourcesRaw): AleoResources {
20+
return {
21+
transparentBalance: new BigNumber(rawResources.transparentBalance),
22+
privateBalance: rawResources.privateBalance ? new BigNumber(rawResources.privateBalance) : null,
23+
provableApi: rawResources.provableApi ? JSON.parse(rawResources.provableApi) : null,
24+
lastPrivateSyncDate: rawResources.lastPrivateSyncDate
25+
? new Date(rawResources.lastPrivateSyncDate)
26+
: null,
27+
unspentPrivateRecords: rawResources.unspentPrivateRecords
28+
? JSON.parse(rawResources.unspentPrivateRecords)
29+
: null,
30+
};
31+
}
32+
33+
export function assignToAccountRaw(account: Account, accountRaw: AccountRaw): void {
34+
const aleoAccount = account as AleoAccount;
35+
const aleoAccountRaw = accountRaw as AleoAccountRaw;
36+
37+
if (aleoAccount.aleoResources) {
38+
aleoAccountRaw.aleoResources = toAleoResourcesRaw(aleoAccount.aleoResources);
39+
}
40+
}
41+
42+
export function assignFromAccountRaw(accountRaw: AccountRaw, account: Account) {
43+
const aleoAccount = account as AleoAccount;
44+
const aleoAccountRaw = accountRaw as AleoAccountRaw;
45+
46+
if (aleoAccountRaw.aleoResources) {
47+
aleoAccount.aleoResources = fromAleoResourcesRaw(aleoAccountRaw.aleoResources);
48+
}
49+
}

libs/coin-modules/coin-aleo/src/bridge/sync.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import { decodeAccountId, encodeAccountId } from "@ledgerhq/coin-framework/accou
99
import { log } from "@ledgerhq/logs";
1010
import { getBalance, lastBlock, listOperations } from "../logic";
1111
import { accessProvableApi } from "../network/utils";
12-
import type { AleoAccount, ProvableApi } from "../types";
12+
import type { AleoAccount, ProvableApi, AleoUnspentRecord } from "../types";
13+
import { getPrivateBalance } from "../logic/getPrivateBalance";
14+
import { isProvableApiConfigured } from "../logic/utils";
15+
import { apiClient } from "../network/api";
1316

1417
export const getAccountShape: GetAccountShape<AleoAccount> = async infos => {
1518
const { initialAccount, address, derivationMode, currency } = infos;
@@ -69,6 +72,32 @@ export const getAccountShape: GetAccountShape<AleoAccount> = async infos => {
6972
},
7073
});
7174

75+
let privateBalance = initialAccount?.aleoResources?.privateBalance ?? null;
76+
let unspentPrivateRecords: AleoUnspentRecord[] | null = null;
77+
let lastPrivateSyncDate = initialAccount?.aleoResources?.lastPrivateSyncDate ?? null;
78+
79+
if (viewKey && isProvableApiConfigured(provableApi)) {
80+
const rawUnspentPrivateRecords = await apiClient.getAccountOwnedRecords({
81+
currency,
82+
jwtToken: provableApi.jwt.token,
83+
uuid: provableApi.uuid,
84+
apiKey: provableApi.apiKey,
85+
unspent: true,
86+
});
87+
88+
const privateBalanceResult = await getPrivateBalance({
89+
currency,
90+
viewKey,
91+
privateRecords: rawUnspentPrivateRecords,
92+
});
93+
94+
privateBalance = privateBalanceResult.balance;
95+
unspentPrivateRecords = privateBalanceResult.unspentRecords;
96+
lastPrivateSyncDate = new Date();
97+
}
98+
99+
const totalBalance = transparentBalance.plus(privateBalance ?? 0);
100+
72101
// sort by date desc
73102
latestAccountPublicOperations.operations.sort((a, b) => b.date.getTime() - a.date.getTime());
74103

@@ -80,15 +109,18 @@ export const getAccountShape: GetAccountShape<AleoAccount> = async infos => {
80109
return {
81110
type: "Account",
82111
id: ledgerAccountId,
83-
balance: transparentBalance,
84-
spendableBalance: transparentBalance,
112+
balance: totalBalance,
113+
spendableBalance: totalBalance,
85114
blockHeight,
86115
operations,
87116
operationsCount: operations.length,
88117
lastSyncDate: new Date(),
89118
aleoResources: {
90119
transparentBalance,
91120
provableApi,
121+
privateBalance,
122+
unspentPrivateRecords,
123+
lastPrivateSyncDate,
92124
},
93125
};
94126
};

0 commit comments

Comments
 (0)