@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.
- package/dist/flowMatrix.d.ts +6 -0
- package/dist/flowMatrix.d.ts.map +1 -0
- package/dist/flowMatrix.js +57 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3819 -0
- package/dist/packing.d.ts +13 -0
- package/dist/packing.d.ts.map +1 -0
- package/dist/packing.js +38 -0
- package/dist/path.d.ts +14 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +153 -0
- package/package.json +36 -0
|
@@ -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"}
|
package/dist/packing.js
ADDED
|
@@ -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
|
+
}
|