@aboutcircles/sdk-pathfinder 0.1.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.
@@ -0,0 +1,13 @@
1
+ import type { TransferStep } from '@aboutcircles/sdk-types';
2
+ /**
3
+ * Pack a uint16 array into a hex string (big‑endian, no padding).
4
+ */
5
+ export declare function packCoordinates(coords: number[]): string;
6
+ /**
7
+ * Build a sorted vertex list plus index lookup for quick coordinate mapping.
8
+ */
9
+ export declare function transformToFlowVertices(transfers: TransferStep[], from: string, to: string): {
10
+ sorted: string[];
11
+ idx: Record<string, number>;
12
+ };
13
+ //# sourceMappingURL=packing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packing.d.ts","sourceRoot":"","sources":["../src/packing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAUxD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,YAAY,EAAE,EACzB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACT;IAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAsBnD"}
@@ -0,0 +1,38 @@
1
+ import { bytesToHex } from '@aboutcircles/sdk-utils';
2
+ /**
3
+ * Pack a uint16 array into a hex string (big‑endian, no padding).
4
+ */
5
+ export function packCoordinates(coords) {
6
+ const bytes = new Uint8Array(coords.length * 2);
7
+ coords.forEach((c, i) => {
8
+ const hi = c >> 8;
9
+ const lo = c & 0xff;
10
+ const offset = 2 * i;
11
+ bytes[offset] = hi;
12
+ bytes[offset + 1] = lo;
13
+ });
14
+ return bytesToHex(bytes);
15
+ }
16
+ /**
17
+ * Build a sorted vertex list plus index lookup for quick coordinate mapping.
18
+ */
19
+ export function transformToFlowVertices(transfers, from, to) {
20
+ const set = new Set([from.toLowerCase(), to.toLowerCase()]);
21
+ transfers.forEach((t) => {
22
+ set.add(t.from.toLowerCase());
23
+ set.add(t.to.toLowerCase());
24
+ set.add(t.tokenOwner.toLowerCase());
25
+ });
26
+ const sorted = [...set].sort((a, b) => {
27
+ const lhs = BigInt(a);
28
+ const rhs = BigInt(b);
29
+ const isLess = lhs < rhs;
30
+ const isGreater = lhs > rhs;
31
+ return isLess ? -1 : isGreater ? 1 : 0;
32
+ });
33
+ const idx = {};
34
+ sorted.forEach((addr, i) => {
35
+ idx[addr] = i;
36
+ });
37
+ return { sorted, idx };
38
+ }
package/dist/path.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { PathfindingResult, Address, TokenInfo } from '@aboutcircles/sdk-types';
2
+ export declare function getTokenInfoMapFromPath(currentAvatar: Address, rpcUrl: string, transferPath: PathfindingResult): Promise<Map<string, TokenInfo>>;
3
+ export declare function getWrappedTokensFromPath(transferPath: PathfindingResult, tokenInfoMap: Map<string, TokenInfo>): Record<string, [bigint, string]>;
4
+ export declare function getExpectedUnwrappedTokenTotals(wrappedTotals: Record<string, [bigint, string]>, tokenInfoMap: Map<string, TokenInfo>): Record<string, [bigint, string]>;
5
+ /**
6
+ * Replace wrapped token addresses with avatar addresses in the path
7
+ * This is used after unwrapping to reflect the actual tokens being transferred
8
+ */
9
+ export declare function replaceWrappedTokensWithAvatars(path: PathfindingResult, tokenInfoMap: Map<string, TokenInfo>): PathfindingResult;
10
+ export declare function replaceWrappedTokens(path: PathfindingResult, unwrapped: Record<string, [bigint, string]>): PathfindingResult;
11
+ export declare function shrinkPathValues(path: PathfindingResult, sink: string, retainBps?: bigint): PathfindingResult;
12
+ export declare function assertNoNettedFlowMismatch(path: PathfindingResult, overrideSource?: string, overrideSink?: string): void;
13
+ export declare function computeNettedFlow(path: PathfindingResult): Map<string, bigint>;
14
+ //# sourceMappingURL=path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../src/path.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAgB,OAAO,EAAE,SAAS,EAAyC,MAAM,yBAAyB,CAAC;AAI1I,wBAAsB,uBAAuB,CAC3C,aAAa,EAAE,OAAO,EACtB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,iBAAiB,GAC9B,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAwBjC;AAED,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,iBAAiB,EAC/B,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GACnC,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAgBlC;AAED,wBAAgB,+BAA+B,CAC7C,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC/C,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GACnC,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAiBlC;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,iBAAiB,EACvB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,GACnC,iBAAiB,CAenB;AAED,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,iBAAiB,EACvB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC1C,iBAAiB,CAmBnB;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,iBAAiB,EACvB,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAAgC,GAC1C,iBAAiB,CAuBnB;AAED,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,iBAAiB,EACvB,cAAc,CAAC,EAAE,MAAM,EACvB,YAAY,CAAC,EAAE,MAAM,GACpB,IAAI,CAkCN;AAmBD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAU9E"}
package/dist/path.js ADDED
@@ -0,0 +1,153 @@
1
+ import { CirclesRpc } from '@aboutcircles/sdk-rpc';
2
+ import { CirclesConverter } from '@aboutcircles/sdk-utils';
3
+ export async function getTokenInfoMapFromPath(currentAvatar, rpcUrl, transferPath) {
4
+ const tokenInfoMap = new Map();
5
+ const uniqueAddresses = new Set();
6
+ transferPath.transfers.forEach((t) => {
7
+ if (currentAvatar.toLowerCase() === t.from.toLowerCase())
8
+ uniqueAddresses.add(t.tokenOwner.toLowerCase());
9
+ });
10
+ const rpc = new CirclesRpc(rpcUrl);
11
+ const batch = await rpc.token.getTokenInfoBatch(Array.from(uniqueAddresses));
12
+ batch.forEach((info) => {
13
+ // @todo temporary fix
14
+ // @dev required to handle wrong returned tokenType from `circles_getTokenInfoBatch`
15
+ if (info.isWrapped && !info.isInflationary) {
16
+ info.tokenType = "CrcV2_ERC20WrapperDeployed_Demurraged";
17
+ }
18
+ tokenInfoMap.set(info.tokenAddress.toLowerCase(), info);
19
+ });
20
+ return tokenInfoMap;
21
+ }
22
+ export function getWrappedTokensFromPath(transferPath, tokenInfoMap) {
23
+ const wrappedTokensInPath = {};
24
+ transferPath.transfers.forEach((t) => {
25
+ const info = tokenInfoMap.get(t.tokenOwner.toLowerCase());
26
+ const isWrapper = info && info.tokenType.startsWith('CrcV2_ERC20WrapperDeployed');
27
+ if (isWrapper) {
28
+ if (!wrappedTokensInPath[t.tokenOwner]) {
29
+ wrappedTokensInPath[t.tokenOwner] = [BigInt(0), info.tokenType];
30
+ }
31
+ wrappedTokensInPath[t.tokenOwner][0] += BigInt(t.value);
32
+ }
33
+ });
34
+ return wrappedTokensInPath;
35
+ }
36
+ export function getExpectedUnwrappedTokenTotals(wrappedTotals, tokenInfoMap) {
37
+ const unwrapped = {};
38
+ Object.entries(wrappedTotals).forEach(([wrapperAddr, [total, type]]) => {
39
+ const info = tokenInfoMap.get(wrapperAddr.toLowerCase());
40
+ if (!info)
41
+ return;
42
+ if (type === 'CrcV2_ERC20WrapperDeployed_Demurraged') {
43
+ unwrapped[wrapperAddr] = [total, info.tokenOwner];
44
+ }
45
+ if (type === 'CrcV2_ERC20WrapperDeployed_Inflationary') {
46
+ unwrapped[wrapperAddr] = [CirclesConverter.attoStaticCirclesToAttoCircles(total), info.tokenOwner];
47
+ }
48
+ });
49
+ return unwrapped;
50
+ }
51
+ /**
52
+ * Replace wrapped token addresses with avatar addresses in the path
53
+ * This is used after unwrapping to reflect the actual tokens being transferred
54
+ */
55
+ export function replaceWrappedTokensWithAvatars(path, tokenInfoMap) {
56
+ const rewritten = path.transfers.map((edge) => {
57
+ // Look up the token info for this tokenOwner
58
+ const tokenInfo = tokenInfoMap.get(edge.tokenOwner.toLowerCase());
59
+ // If we have token info and it's a wrapped token, replace with the underlying avatar
60
+ if (tokenInfo && tokenInfo.tokenType.startsWith('CrcV2_ERC20WrapperDeployed')) {
61
+ return { ...edge, tokenOwner: tokenInfo.tokenOwner };
62
+ }
63
+ // Keep the original tokenOwner if it's not wrapped
64
+ return edge;
65
+ });
66
+ return { ...path, transfers: rewritten };
67
+ }
68
+ export function replaceWrappedTokens(path, unwrapped) {
69
+ // Create a mapping from wrapped token addresses to avatar addresses
70
+ // unwrapped format: { wrapperAddress: [amount, avatarAddress] }
71
+ const wrapperToAvatar = {};
72
+ Object.entries(unwrapped).forEach(([wrapperAddr, [, avatarAddr]]) => {
73
+ wrapperToAvatar[wrapperAddr.toLowerCase()] = avatarAddr;
74
+ });
75
+ const rewritten = path.transfers.map((edge) => {
76
+ // Replace tokenOwner if it's a wrapped token address
77
+ // This changes which token is being transferred (from wrapped to underlying avatar token)
78
+ const tokenOwnerLower = edge.tokenOwner.toLowerCase();
79
+ const tokenOwner = (wrapperToAvatar[tokenOwnerLower] || edge.tokenOwner);
80
+ // Keep from and to addresses unchanged - they represent the actual flow participants
81
+ return { ...edge, tokenOwner };
82
+ });
83
+ return { ...path, transfers: rewritten };
84
+ }
85
+ export function shrinkPathValues(path, sink, retainBps = BigInt(999_999_999_999)) {
86
+ const incomingToSink = new Map();
87
+ const scaled = [];
88
+ const DENOM = BigInt(1_000_000_000_000);
89
+ path.transfers.forEach((edge) => {
90
+ const scaledValue = (BigInt(edge.value) * retainBps) / DENOM;
91
+ const isZero = scaledValue === BigInt(0);
92
+ if (isZero) {
93
+ return; // drop sub‑unit flows
94
+ }
95
+ scaled.push({ ...edge, value: scaledValue });
96
+ incomingToSink.set(edge.to, (incomingToSink.get(edge.to) ?? BigInt(0)) + scaledValue);
97
+ });
98
+ const maxFlow = sink ? incomingToSink.get(sink.toLowerCase()) ?? BigInt(0) : BigInt(0);
99
+ return {
100
+ maxFlow: maxFlow,
101
+ transfers: scaled
102
+ };
103
+ }
104
+ export function assertNoNettedFlowMismatch(path, overrideSource, overrideSink) {
105
+ const net = computeNettedFlow(path);
106
+ const { source, sink } = getSourceAndSink(path, overrideSource, overrideSink);
107
+ const endpointsCoincide = source === sink;
108
+ net.forEach((balance, addr) => {
109
+ /* ----------------------------------------------------------------
110
+ * Closed-loop case → every vertex must net to zero
111
+ * -------------------------------------------------------------- */
112
+ if (endpointsCoincide) {
113
+ if (balance !== BigInt(0)) {
114
+ throw new Error(`Vertex ${addr} is unbalanced: ${balance}`);
115
+ }
116
+ return; // done – nothing else to check for this addr
117
+ }
118
+ /* ----------------------------------------------------------------
119
+ * Ordinary DAG case → classic source / sink / intermediate rules
120
+ * -------------------------------------------------------------- */
121
+ const isSource = addr === source;
122
+ const isSink = addr === sink;
123
+ if (isSource && balance >= BigInt(0)) {
124
+ throw new Error(`Source ${addr} should be net negative, got ${balance}`);
125
+ }
126
+ if (isSink && balance <= BigInt(0)) {
127
+ throw new Error(`Sink ${addr} should be net positive, got ${balance}`);
128
+ }
129
+ const isIntermediate = !isSource && !isSink;
130
+ if (isIntermediate && balance !== BigInt(0)) {
131
+ throw new Error(`Vertex ${addr} is unbalanced: ${balance}`);
132
+ }
133
+ });
134
+ }
135
+ function getSourceAndSink(path, overrideSource, overrideSink) {
136
+ const senders = new Set(path.transfers.map((t) => t.from.toLowerCase()));
137
+ const receivers = new Set(path.transfers.map((t) => t.to.toLowerCase()));
138
+ const source = [...senders].find((a) => !receivers.has(a));
139
+ const sink = [...receivers].find((a) => !senders.has(a));
140
+ if (!(source ?? overrideSource) || !(sink ?? overrideSink)) {
141
+ throw new Error('Could not determine unique source / sink');
142
+ }
143
+ return { source: (source ?? overrideSource), sink: (sink ?? overrideSink) };
144
+ }
145
+ export function computeNettedFlow(path) {
146
+ const net = new Map();
147
+ path.transfers.forEach(({ from, to, value }) => {
148
+ const amount = BigInt(value);
149
+ net.set(from.toLowerCase(), (net.get(from.toLowerCase()) ?? BigInt(0)) - amount);
150
+ net.set(to.toLowerCase(), (net.get(to.toLowerCase()) ?? BigInt(0)) + amount);
151
+ });
152
+ return net;
153
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@aboutcircles/sdk-pathfinder",
3
+ "version": "0.1.0",
4
+ "description": "Pathfinding utilities for Circles SDK",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "bun build ./src/index.ts --outdir ./dist && tsc --emitDeclarationOnly",
16
+ "dev": "tsc --build --watch",
17
+ "clean": "rm -rf dist tsconfig.tsbuildinfo"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "keywords": [
23
+ "circles",
24
+ "pathfinding"
25
+ ],
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@aboutcircles/sdk-rpc": "*",
29
+ "@aboutcircles/sdk-types": "*",
30
+ "@aboutcircles/sdk-utils": "*",
31
+ "viem": "^2.38.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.0.4"
35
+ }
36
+ }