Skip to content

Commit c447cad

Browse files
authored
Add confirmed balance (#36)
1 parent 02f66db commit c447cad

File tree

5 files changed

+153
-7
lines changed

5 files changed

+153
-7
lines changed

packages/mobile-app/app/(tabs)/index.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ export default function Balances() {
4747
</ScrollView>
4848
<Text style={{ fontWeight: 700, fontSize: 24 }}>Balance</Text>
4949
{getAccountResult.data && (
50-
<Text>{`IRON ${getAccountResult.data.balances.iron.unconfirmed}`}</Text>
50+
<>
51+
<Text>{`Unconfirmed: IRON ${getAccountResult.data.balances.iron.unconfirmed}`}</Text>
52+
<Text>{`Confirmed: IRON ${getAccountResult.data.balances.iron.confirmed}`}</Text>
53+
</>
5154
)}
5255
<StatusBar style="auto" />
5356
</View>

packages/mobile-app/data/api/walletServer.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,23 @@ const WALLET_SERVER_URLS: Record<Network, string> = {
1616
class WalletServer {
1717
transformers: WalletServerTransformer[] = [ForkTester];
1818

19+
private latestBlockCache: Map<
20+
Network,
21+
{ time: number; response: GetLatestBlockResponse }
22+
> = new Map();
23+
private LATEST_BLOCK_CACHE_MS = 5000;
24+
1925
async getLatestBlock(network: Network): Promise<GetLatestBlockResponse> {
2026
const url = WALLET_SERVER_URLS[network] + "latest-block";
27+
28+
const cached = this.latestBlockCache.get(network);
29+
if (
30+
cached &&
31+
performance.now() - cached.time < this.LATEST_BLOCK_CACHE_MS
32+
) {
33+
return cached.response;
34+
}
35+
2136
console.log("requesting latest block");
2237

2338
const fetchResult = await fetch(url);
@@ -27,6 +42,11 @@ class WalletServer {
2742
latestBlock = await transformer.getLatestBlock(network, latestBlock);
2843
}
2944

45+
this.latestBlockCache.set(network, {
46+
time: performance.now(),
47+
response: latestBlock,
48+
});
49+
3050
return latestBlock;
3151
}
3252

packages/mobile-app/data/facades/wallet/handlers.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,24 +86,24 @@ export const walletHandlers = f.facade<WalletHandlers>({
8686
"51f33a2f14f92735e562dc658a5639279ddca3d5079a6d1242b2a588a9cbf44c",
8787
// TODO: Implement available balance in Wallet
8888
available: "0",
89-
// TODO: Implement confirmed balance in Wallet
90-
confirmed: "0",
9189
// TODO: Implement pending balance in Wallet
9290
pending: "0",
9391
unconfirmed: "0",
92+
confirmed: "0",
9493
};
9594
const customBalances: AccountBalance[] = [];
9695

9796
for (const balance of balances) {
9897
if (Uint8ArrayUtils.toHex(balance.assetId) === ironBalance.assetId) {
99-
ironBalance.unconfirmed = balance.value;
98+
ironBalance.unconfirmed = balance.unconfirmed;
99+
ironBalance.confirmed = balance.confirmed;
100100
} else {
101101
customBalances.push({
102102
assetId: Uint8ArrayUtils.toHex(balance.assetId),
103103
available: "0",
104-
confirmed: "0",
105104
pending: "0",
106-
unconfirmed: balance.value,
105+
confirmed: balance.confirmed,
106+
unconfirmed: balance.unconfirmed,
107107
});
108108
}
109109
}

packages/mobile-app/data/wallet/db.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ interface AccountTransactionsTable {
3838
transactionType: string;
3939
}
4040

41+
interface TransactionBalanceDeltasTable {
42+
id: Generated<number>;
43+
accountId: number;
44+
transactionHash: Uint8Array;
45+
assetId: Uint8Array;
46+
value: string;
47+
}
48+
4149
interface NotesTable {
4250
id: Generated<number>;
4351
accountId: number;
@@ -85,6 +93,7 @@ interface Database {
8593
accountNetworkHeads: AccountNetworkHeadsTable;
8694
transactions: TransactionsTable;
8795
accountTransactions: AccountTransactionsTable;
96+
transactionBalanceDeltas: TransactionBalanceDeltasTable;
8897
notes: NotesTable;
8998
nullifiers: NullifiersTable;
9099
balances: BalancesTable;
@@ -295,6 +304,30 @@ export class WalletDb {
295304
console.log("created balances");
296305
},
297306
},
307+
["008_createTransactionBalanceDeltas"]: {
308+
up: async (db: Kysely<Database>) => {
309+
console.log("creating transaction balance deltas");
310+
await db.schema
311+
.createTable("transactionBalanceDeltas")
312+
.addColumn("id", SQLiteType.Integer, (col) =>
313+
col.primaryKey().autoIncrement(),
314+
)
315+
.addColumn("accountId", SQLiteType.Integer, (col) =>
316+
col.notNull().references("accounts.id"),
317+
)
318+
.addColumn("transactionHash", SQLiteType.Blob, (col) =>
319+
col.notNull().references("transactions.hash"),
320+
)
321+
.addColumn("assetId", SQLiteType.Blob, (col) => col.notNull())
322+
.addColumn("value", SQLiteType.String, (col) => col.notNull())
323+
.addUniqueConstraint(
324+
"transactionbalancedeltas_accountId_hash_assetId",
325+
["accountId", "transactionHash", "assetId"],
326+
)
327+
.execute();
328+
console.log("created transaction balance deltas");
329+
},
330+
},
298331
},
299332
}),
300333
});
@@ -658,6 +691,16 @@ export class WalletDb {
658691
.doUpdateSet({ value: (existingBalance + delta[1]).toString() }),
659692
)
660693
.executeTakeFirstOrThrow();
694+
695+
await db
696+
.insertInto("transactionBalanceDeltas")
697+
.values({
698+
accountId: values.accountId,
699+
assetId: Uint8ArrayUtils.fromHex(delta[0]),
700+
transactionHash: values.hash,
701+
value: delta[1].toString(),
702+
})
703+
.executeTakeFirstOrThrow();
661704
}
662705
});
663706
}
@@ -773,4 +816,29 @@ export class WalletDb {
773816
)
774817
.execute();
775818
}
819+
820+
async getTransactionBalanceDeltasBySequence(
821+
accountId: number,
822+
network: Network,
823+
startSequence: number,
824+
endSequence: number,
825+
) {
826+
return await this.db
827+
.selectFrom("transactionBalanceDeltas")
828+
.innerJoin(
829+
"transactions",
830+
"transactions.hash",
831+
"transactionBalanceDeltas.transactionHash",
832+
)
833+
.selectAll()
834+
.where((eb) =>
835+
eb.and([
836+
eb("transactions.blockSequence", ">=", startSequence),
837+
eb("transactions.blockSequence", "<=", endSequence),
838+
eb("transactions.network", "=", network),
839+
eb("transactionBalanceDeltas.accountId", "=", accountId),
840+
]),
841+
)
842+
.execute();
843+
}
776844
}

packages/mobile-app/data/wallet/wallet.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { Network } from "../constants";
1212
import * as Uint8ArrayUtils from "../../utils/uint8Array";
1313
import { LightBlock, LightTransaction } from "../api/lightstreamer";
1414
import { WriteCache } from "./writeCache";
15+
import { WalletServerApi } from "../api/walletServer";
1516

1617
type StartedState = { type: "STARTED"; db: WalletDb };
1718
type WalletState = { type: "STOPPED" } | { type: "LOADING" } | StartedState;
@@ -80,7 +81,61 @@ class Wallet {
8081
async getBalances(accountId: number, network: Network) {
8182
assertStarted(this.state);
8283

83-
return this.state.db.getBalances(accountId, network);
84+
const unconfirmed = await this.state.db.getBalances(accountId, network);
85+
86+
const deltas = await this.getUnconfirmedDeltas(accountId, network);
87+
const confirmed: { assetId: Uint8Array; value: string }[] = unconfirmed.map(
88+
(balance) => {
89+
const assetDeltas = deltas.filter((d) =>
90+
Uint8ArrayUtils.areEqual(d.assetId, balance.assetId),
91+
);
92+
93+
return {
94+
assetId: balance.assetId,
95+
value: assetDeltas
96+
.reduce((a, b) => {
97+
return a - BigInt(b.value);
98+
}, BigInt(balance.value))
99+
.toString(),
100+
};
101+
},
102+
);
103+
104+
// Caution, this makes the assumption that unconfirmed and confirmed are ordered the same
105+
const balanceReturns = [];
106+
for (let i = 0; i < unconfirmed.length; i++) {
107+
balanceReturns.push({
108+
assetId: unconfirmed[i].assetId,
109+
confirmed: confirmed[i].value,
110+
unconfirmed: unconfirmed[i].value,
111+
});
112+
}
113+
114+
return balanceReturns;
115+
}
116+
117+
/**
118+
* Returns transaction balance deltas from head - confirmationRange + 1 to head, inclusive.
119+
*/
120+
private async getUnconfirmedDeltas(accountId: number, network: Network) {
121+
assertStarted(this.state);
122+
123+
const chainHead = (await WalletServerApi.getLatestBlock(network)).sequence;
124+
// TODO: Make confirmation range configurable
125+
const confirmationRange = 2;
126+
127+
if (confirmationRange <= 0) {
128+
return [];
129+
}
130+
131+
const deltas = await this.state.db.getTransactionBalanceDeltasBySequence(
132+
accountId,
133+
network,
134+
chainHead - confirmationRange + 1,
135+
chainHead,
136+
);
137+
138+
return deltas;
84139
}
85140

86141
async exportAccount(

0 commit comments

Comments
 (0)