From 8d220ceb39086bbe8af571c440b3b6a8b7913a97 Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Sun, 7 Jul 2024 16:05:29 +0200 Subject: [PATCH 1/8] 0.4.0-profiles-preview-1: * ! Breaking change: Renamed ChainConfig -> CirclesConfig and added a `profileServiceUrl` * ! Breaking change: Require `Profile` instances instead of CIDv0s for all `register*` methods * calculated columns in CirclesQuery can be async * return profile CID from getAvatarInfo() * Add `getEvents()` method to CirclesData * Add a new 'profiles' package that creates/gets profiles --- README.md | 7 +- package-lock.json | 155 +++++++++--------- package.json | 12 +- packages/abi-v1/package.json | 2 +- packages/abi-v2/package.json | 2 +- packages/data/package.json | 4 +- packages/data/src/circlesData.ts | 38 ++++- packages/data/src/circlesQueryRpcResult.ts | 4 + packages/data/src/circlesRpc.ts | 10 -- packages/data/src/events/parser.ts | 6 +- packages/data/src/pagedQuery/circlesQuery.ts | 23 +-- packages/data/src/pagedQuery/cursor.ts | 8 +- packages/data/src/pagedQuery/eventRow.ts | 2 +- packages/data/src/rows/avatarRow.ts | 7 +- packages/profiles/package.json | 25 +++ packages/profiles/rollup.config.js | 20 +++ packages/profiles/src/index.ts | 45 +++++ packages/profiles/tsconfig.json | 9 + packages/sdk/package.json | 10 +- packages/sdk/rollup.config.js | 2 +- .../src/{chainConfig.ts => circlesConfig.ts} | 3 +- packages/sdk/src/index.ts | 2 +- packages/sdk/src/sdk.ts | 142 ++++++++++------ packages/sdk/src/v2/v2Person.ts | 7 +- packages/tests/package-lock.json | 16 +- packages/tests/package.json | 1 + packages/tests/test/data/circlesData.test.ts | 9 +- packages/tests/test/sdk/v1/v1Avatar.test.ts | 4 +- packages/tests/test/utils/utils.test.ts | 30 ++++ packages/utils/package.json | 5 +- packages/utils/rollup.config.js | 2 +- packages/utils/src/index.ts | 58 +++++++ 32 files changed, 481 insertions(+), 189 deletions(-) create mode 100644 packages/data/src/circlesQueryRpcResult.ts create mode 100644 packages/profiles/package.json create mode 100644 packages/profiles/rollup.config.js create mode 100644 packages/profiles/src/index.ts create mode 100644 packages/profiles/tsconfig.json rename packages/sdk/src/{chainConfig.ts => circlesConfig.ts} (75%) create mode 100644 packages/tests/test/utils/utils.test.ts diff --git a/README.md b/README.md index ecd3634..36655bf 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,13 @@ _NOTE_: The node at `circlesRpcUrl` must have the [circles-nethermind-plugin](https://github.com/CirclesUBI/circles-nethermind-plugin) installed. ```typescript -import { ChainConfig } from '@circles-sdk/sdk'; +import { CirclesConfig } from '@circles-sdk/sdk'; // Chiado testnet: -export const chainConfig: ChainConfig = { +export const chainConfig: CirclesConfig = { pathfinderUrl: 'https://pathfinder.aboutcircles.com', circlesRpcUrl: 'https://chiado-rpc.aboutcircles.com', + profileServiceUrl: '', v1HubAddress: '0xdbf22d4e8962db3b2f1d9ff55be728a887e47710', v2HubAddress: '0x2066CDA98F98397185483aaB26A89445addD6740', migrationAddress: '0x2A545B54bb456A0189EbC53ed7090BfFc4a6Af94' @@ -97,7 +98,7 @@ interface SdkInterface { /** * The chain specific Circles configuration (contract addresses and rpc endpoints). */ - chainConfig: ChainConfig; + chainConfig: CirclesConfig; /** * A configured instance of the CirclesData class, an easy-to-use wrapper around * the Circles RPC Query API. diff --git a/package-lock.json b/package-lock.json index 9b07f44..6854864 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,11 +10,13 @@ "packages/abi-v2", "packages/sdk", "packages/data", - "packages/utils" + "packages/utils", + "packages/profiles" ], "dependencies": { "@openzeppelin/contracts": "^3.4.2", - "bignumber.js": "^9.1.2" + "bignumber.js": "^9.1.2", + "bs58": "^6.0.0" }, "devDependencies": { "@rollup/plugin-json": "^6.1.0", @@ -631,6 +633,10 @@ "resolved": "packages/data", "link": true }, + "node_modules/@circles-sdk/profiles": { + "resolved": "packages/profiles", + "link": true + }, "node_modules/@circles-sdk/sdk": { "resolved": "packages/sdk", "link": true @@ -1417,6 +1423,12 @@ "dev": true, "license": "MIT" }, + "node_modules/base-x": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-5.0.0.tgz", + "integrity": "sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==", + "license": "MIT" + }, "node_modules/bignumber.js": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", @@ -1435,11 +1447,13 @@ } }, "node_modules/braces": { - "version": "3.0.2", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1487,6 +1501,15 @@ "node": ">= 6" } }, + "node_modules/bs58": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-6.0.0.tgz", + "integrity": "sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==", + "license": "MIT", + "dependencies": { + "base-x": "^5.0.0" + } + }, "node_modules/bser": { "version": "2.1.1", "dev": true, @@ -1500,19 +1523,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bufferutil": { - "version": "4.0.8", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -1932,7 +1942,9 @@ "license": "MIT" }, "node_modules/ethers": { - "version": "6.11.1", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.13.1.tgz", + "integrity": "sha512-hdJ2HOxg/xx97Lm9HdCWk949BfYqYWpyw4//78SiwOLgASyfrNszfMUNB2joKjvGUdwhHfaiMMFFwacVVoLR9A==", "funding": [ { "type": "individual", @@ -1951,7 +1963,7 @@ "@types/node": "18.15.13", "aes-js": "4.0.0-beta.5", "tslib": "2.4.0", - "ws": "8.5.0" + "ws": "8.17.1" }, "engines": { "node": ">=14.0.0" @@ -1989,25 +2001,6 @@ "version": "2.4.0", "license": "0BSD" }, - "node_modules/ethers/node_modules/ws": { - "version": "8.5.0", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/execa": { "version": "5.1.1", "dev": true, @@ -2066,7 +2059,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -2321,6 +2316,8 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -3250,17 +3247,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-gyp-build": { - "version": "4.8.0", - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" - } - }, "node_modules/node-int64": { "version": "0.4.0", "dev": true, @@ -3834,6 +3820,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4079,19 +4067,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/utf-8-validate": { - "version": "5.0.10", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "node-gyp-build": "^4.3.0" - }, - "engines": { - "node": ">=6.14.2" - } - }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "dev": true, @@ -4186,6 +4161,27 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/y18n": { "version": "5.0.8", "dev": true, @@ -4237,7 +4233,7 @@ }, "packages/abi-v1": { "name": "@circles-sdk/abi-v1", - "version": "0.3.0", + "version": "0.4.0-profiles-preview-1", "license": "ISC", "dependencies": { "ethers": "^6.11.1" @@ -4248,7 +4244,7 @@ }, "packages/abi-v2": { "name": "@circles-sdk/abi-v2", - "version": "0.3.0", + "version": "0.4.0-profiles-preview-1", "license": "ISC", "dependencies": { "ethers": "^6.11.1" @@ -4259,10 +4255,20 @@ }, "packages/data": { "name": "@circles-sdk/data", - "version": "0.3.0", + "version": "0.4.0-profiles-preview-1", "license": "ISC", "dependencies": { - "@circles-sdk/utils": "0.3.0" + "@circles-sdk/utils": "0.4.0-profiles-preview-1" + }, + "devDependencies": { + "typescript": "^5.3.3" + } + }, + "packages/profiles": { + "version": "0.4.0-profiles-preview-1", + "license": "ISC", + "dependencies": { + "@circles-sdk/utils": "0.4.0-profiles-preview-1" }, "devDependencies": { "typescript": "^5.3.3" @@ -4270,12 +4276,14 @@ }, "packages/sdk": { "name": "@circles-sdk/sdk", - "version": "0.3.0", + "version": "0.4.0-profiles-preview-1", "license": "ISC", "dependencies": { - "@circles-sdk/abi-v1": "0.3.0", - "@circles-sdk/abi-v2": "0.3.0", - "@circles-sdk/data": "0.3.0", + "@circles-sdk/abi-v1": "0.4.0-profiles-preview-1", + "@circles-sdk/abi-v2": "0.4.0-profiles-preview-1", + "@circles-sdk/data": "0.4.0-profiles-preview-1", + "@circles-sdk/profiles": "0.4.0-profiles-preview-1", + "@circles-sdk/utils": "0.4.0-profiles-preview-1", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, @@ -4285,10 +4293,11 @@ }, "packages/utils": { "name": "@circles-sdk/utils", - "version": "0.3.0", + "version": "0.4.0-profiles-preview-1", "license": "ISC", "dependencies": { - "bignumber.js": "^9.1.2" + "bignumber.js": "^9.1.2", + "multihashes": "^4.0.3" }, "devDependencies": { "typescript": "^5.3.3" diff --git a/package.json b/package.json index 552d3c7..8fcdaf4 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,14 @@ "packages/abi-v2", "packages/sdk", "packages/data", - "packages/utils" + "packages/utils", + "packages/profiles" ], "scripts": { "test": "jest", - "build": "./generateCode.sh && npm run build -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/utils -w @circles-sdk/data -w @circles-sdk/sdk", - "publish": "npm run build && npm publish -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/data -w @circles-sdk/utils -w @circles-sdk/sdk", - "publish:dry-run": "npm run build && npm publish --dry-run -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/data -w @circles-sdk/sdk -w @circles-sdk/utils" + "build": "./generateCode.sh && npm run build -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/utils -w @circles-sdk/data -w @circles-sdk/sdk -w @circles-sdk/profiles", + "publish": "npm run build && npm publish -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/data -w @circles-sdk/utils -w @circles-sdk/profiles -w @circles-sdk/sdk", + "publish:dry-run": "npm run build && npm publish --dry-run -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/data -w @circles-sdk/sdk -w @circles-sdk/utils -w @circles-sdk/profiles" }, "devDependencies": { "@rollup/plugin-json": "^6.1.0", @@ -32,6 +33,7 @@ "version": "", "dependencies": { "@openzeppelin/contracts": "^3.4.2", - "bignumber.js": "^9.1.2" + "bignumber.js": "^9.1.2", + "bs58": "^6.0.0" } } diff --git a/packages/abi-v1/package.json b/packages/abi-v1/package.json index 719ca24..f4a00be 100644 --- a/packages/abi-v1/package.json +++ b/packages/abi-v1/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/abi-v1", - "version": "0.3.3", + "version": "0.4.0-profiles-preview-1", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/abi-v2/package.json b/packages/abi-v2/package.json index d828e82..4a4d327 100644 --- a/packages/abi-v2/package.json +++ b/packages/abi-v2/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/abi-v2", - "version": "0.3.3", + "version": "0.4.0-profiles-preview-1", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/data/package.json b/packages/data/package.json index 1acff34..9ae437a 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/data", - "version": "0.3.3", + "version": "0.4.0-profiles-preview-1", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/utils": "0.3.3" + "@circles-sdk/utils": "0.4.0-profiles-preview-1" }, "keywords": [], "author": "", diff --git a/packages/data/src/circlesData.ts b/packages/data/src/circlesData.ts index 92333d4..47ce7da 100644 --- a/packages/data/src/circlesData.ts +++ b/packages/data/src/circlesData.ts @@ -4,7 +4,7 @@ import { TrustListRow } from './rows/trustListRow'; import { TokenBalanceRow } from './rows/tokenBalanceRow'; import { CirclesRpc } from './circlesRpc'; import { AvatarRow } from './rows/avatarRow'; -import { crcToTc } from '@circles-sdk/utils'; +import { crcToTc, hexStringToUint8Array, uint8ArrayToCidV0 } from '@circles-sdk/utils'; import { ethers } from 'ethers'; import { TrustRelation, TrustRelationRow } from './rows/trustRelationRow'; import { CirclesDataInterface, GroupQueryParams } from './circlesDataInterface'; @@ -16,6 +16,7 @@ import { Filter } from './rpcSchema/filter'; import { GroupMembershipRow } from './rows/groupMembershipRow'; import { GroupRow } from './rows/groupRow'; import { TokenInfoRow } from './rows/tokenInfoRow'; +import { parseRpcSubscriptionMessage, RcpSubscriptionEvent } from './events/parser'; export class CirclesData implements CirclesDataInterface { readonly rpc: CirclesRpc; @@ -112,7 +113,7 @@ export class CirclesData implements CirclesDataInterface { ] }, [{ name: 'timeCircles', - generator: (row: TransactionHistoryRow) => { + generator: async (row: TransactionHistoryRow) => { if (row.version === 1) { const timestamp = new Date(row.timestamp * 1000); return crcToTc(timestamp, BigInt(row.value)).toFixed(2); @@ -122,7 +123,7 @@ export class CirclesData implements CirclesDataInterface { } }, { name: 'tokenAddress', - generator: (row: TransactionHistoryRow) => { + generator: async (row: TransactionHistoryRow) => { // If the id isset, doesn't start with 0x and only consists of digits, it's a BigInt that // needs to be converted to a ethereum address. The BigInt is actually an encoded byte[20] // that represents the address. @@ -271,7 +272,22 @@ export class CirclesData implements CirclesDataInterface { ], sortOrder: 'ASC', limit: 1000 - }); + }, [{ + name: 'cidV0', + generator: async (row: AvatarRow) => { + try { + if (!row.cidV0Digest) { + return undefined; + } + + const dataFromHexString = hexStringToUint8Array(row.cidV0Digest.substring(2)); + return uint8ArrayToCidV0(dataFromHexString); + } catch (error) { + console.error('Failed to convert cidV0Digest to CIDv0 string:', error); + return undefined; + } + } + }]); if (!await circlesQuery.queryNextPage()) { return undefined; @@ -342,6 +358,20 @@ export class CirclesData implements CirclesDataInterface { return this.rpc.subscribe(avatar); } + /** + * Gets the events for a given avatar in a block range. + * @param avatar The avatar to get the events for. + * @param fromBlock The block number to start from. + * @param toBlock The block number to end at. If not provided, the latest block is used. + */ + async getEvents(avatar: string, fromBlock: number, toBlock?: number): Promise { + const response = await this.rpc.call( + 'circles_events', + [avatar, fromBlock, toBlock] + ); + return parseRpcSubscriptionMessage(response.result); + } + /** * Gets the invitations sent by an avatar. * @param avatar The avatar to get the invitations for. diff --git a/packages/data/src/circlesQueryRpcResult.ts b/packages/data/src/circlesQueryRpcResult.ts new file mode 100644 index 0000000..3f2264d --- /dev/null +++ b/packages/data/src/circlesQueryRpcResult.ts @@ -0,0 +1,4 @@ +export type CirclesQueryRpcResult = { + columns: string[]; + rows: any[][]; +}; \ No newline at end of file diff --git a/packages/data/src/circlesRpc.ts b/packages/data/src/circlesRpc.ts index b8a966c..a55fc41 100644 --- a/packages/data/src/circlesRpc.ts +++ b/packages/data/src/circlesRpc.ts @@ -121,14 +121,4 @@ export class CirclesRpc { // TODO: Add unsubscribe method to observable return observable.property; } -} - -export type CirclesQueryRpcResult = { - columns: string[]; - rows: any[][]; -}; - -export type RawWebsocketEvent = { - event: string; - values: Record; } \ No newline at end of file diff --git a/packages/data/src/events/parser.ts b/packages/data/src/events/parser.ts index b34c1b7..2d72236 100644 --- a/packages/data/src/events/parser.ts +++ b/packages/data/src/events/parser.ts @@ -4,10 +4,12 @@ type EventValues = { [key: string]: string; }; -type RpcSubscriptionMessage = Array<{ +export type RcpSubscriptionEvent = { event: string; values: EventValues; -}>; +}; + +type RpcSubscriptionMessage = RcpSubscriptionEvent[]; const hexToBigInt = (hex: string): bigint => BigInt(hex); const hexToNumber = (hex: string): number => parseInt(hex, 16); diff --git a/packages/data/src/pagedQuery/circlesQuery.ts b/packages/data/src/pagedQuery/circlesQuery.ts index a44dece..3b90f4a 100644 --- a/packages/data/src/pagedQuery/circlesQuery.ts +++ b/packages/data/src/pagedQuery/circlesQuery.ts @@ -6,11 +6,12 @@ import { Filter } from '../rpcSchema/filter'; import { Order } from '../rpcSchema/order'; import { PagedQueryResult } from './pagedQueryResult'; import { EventRow } from './eventRow'; -import { CirclesQueryRpcResult, CirclesRpc } from '../circlesRpc'; +import { CirclesRpc } from '../circlesRpc'; +import { CirclesQueryRpcResult } from '../circlesQueryRpcResult'; export class CalculatedColumn { constructor(public readonly name: string - , public readonly generator: (row: any) => any) { + , public readonly generator: (row: any) => Promise) { } } @@ -244,7 +245,7 @@ export class CirclesQuery { */ private async request(method: string, param: CirclesQueryParams): Promise { const jsonResponse = await this.rpc.call(method, [param]); - return this.rowsToObjects(jsonResponse); + return await this.rowsToObjects(jsonResponse); } /** @@ -252,7 +253,7 @@ export class CirclesQuery { * @param jsonResponse The JSON-RPC response. * @private */ - private rowsToObjects(jsonResponse: JsonRpcResponse): TRow[] { + private async rowsToObjects(jsonResponse: JsonRpcResponse): Promise { const { columns, rows } = jsonResponse.result; const calculatedColumns = Object.entries(this._calculatedColumns); @@ -260,18 +261,20 @@ export class CirclesQuery { calculatedColumns.forEach(col => columns.push(col[0])); } - return rows.map(row => { + const rowObjects = await Promise.all(rows.map(async row => { const rowObj: Record = {}; row.forEach((value, index) => { rowObj[columns[index]] = value; }); for (const [name, column] of calculatedColumns) { - rowObj[name] = column.generator(rowObj); + rowObj[name] = await column.generator(rowObj); } return rowObj; - }); + })); + + return rowObjects as TRow[]; } /** @@ -342,9 +345,9 @@ export class CirclesQuery { return result.length > 0; } -/** - * Queries a single row from the Circles RPC node. - */ + /** + * Queries a single row from the Circles RPC node. + */ public async getSingleRow(): Promise { const orderBy = this.buildOrderBy(this.params); const filter = this.buildCursorFilter(this.params, this._currentPage?.lastCursor); diff --git a/packages/data/src/pagedQuery/cursor.ts b/packages/data/src/pagedQuery/cursor.ts index 6621d1a..fd499e7 100644 --- a/packages/data/src/pagedQuery/cursor.ts +++ b/packages/data/src/pagedQuery/cursor.ts @@ -1,9 +1,7 @@ +import { EventRow } from './eventRow'; + /** * A cursor is a sortable unique identifier for a specific log entry. */ -export interface Cursor { - readonly blockNumber: number; - readonly transactionIndex: number; - readonly logIndex: number; - readonly batchIndex?: number; +export interface Cursor extends EventRow { } \ No newline at end of file diff --git a/packages/data/src/pagedQuery/eventRow.ts b/packages/data/src/pagedQuery/eventRow.ts index d86da56..9135486 100644 --- a/packages/data/src/pagedQuery/eventRow.ts +++ b/packages/data/src/pagedQuery/eventRow.ts @@ -6,5 +6,5 @@ export interface EventRow { blockNumber: number; transactionIndex: number; logIndex: number; - batchIndex: number | undefined; + batchIndex?: number; } \ No newline at end of file diff --git a/packages/data/src/rows/avatarRow.ts b/packages/data/src/rows/avatarRow.ts index a52192f..ac9bea8 100644 --- a/packages/data/src/rows/avatarRow.ts +++ b/packages/data/src/rows/avatarRow.ts @@ -42,10 +42,15 @@ export interface AvatarRow extends EventRow { */ v1Token?: string; /** - * The CIDv0 of the avatar's metadata (profile). + * The bytes of the avatar's metadata cidv0. */ cidV0Digest?: string; + /** + * The CIDv0 of the avatar's metadata (profile + */ + cidV0?: string; + /** * If the avatar is stopped in v1. * diff --git a/packages/profiles/package.json b/packages/profiles/package.json new file mode 100644 index 0000000..1852b89 --- /dev/null +++ b/packages/profiles/package.json @@ -0,0 +1,25 @@ +{ + "name": "@circles-sdk/profiles", + "version": "0.4.0-profiles-preview-1", + "description": "", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "package.json" + ], + "devDependencies": { + "typescript": "^5.3.3" + }, + "scripts": { + "build": "rollup -c" + }, + "dependencies": { + "@circles-sdk/utils": "0.4.0-profiles-preview-1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/profiles/rollup.config.js b/packages/profiles/rollup.config.js new file mode 100644 index 0000000..6189dc1 --- /dev/null +++ b/packages/profiles/rollup.config.js @@ -0,0 +1,20 @@ +import typescript from '@rollup/plugin-typescript'; +import json from '@rollup/plugin-json'; + +export default { + input: './src/index.ts', + output: [ + { + dir: 'dist', + format: 'es', + sourcemap: true + } + ], + plugins: [ + json(), + typescript({ + tsconfig: './tsconfig.json' + }) + ], + external: ['@circles-sdk/utils'] +}; diff --git a/packages/profiles/src/index.ts b/packages/profiles/src/index.ts new file mode 100644 index 0000000..24421f9 --- /dev/null +++ b/packages/profiles/src/index.ts @@ -0,0 +1,45 @@ +export interface Profile { + name: string; + description?: string; + previewImageUrl?: string; + imageUrl?: string; +} + +export interface GroupProfile extends Profile { + symbol: string; +} + +export class Profiles { + constructor(private readonly profileServiceUrl: string) { + } + + private getProfileServiceUrl(): string { + return this.profileServiceUrl.endsWith('/') ? this.profileServiceUrl : `${this.profileServiceUrl}/`; + } + + async create(profile: Profile): Promise { + const profileServiceUrl = this.profileServiceUrl.endsWith('/') ? this.profileServiceUrl : `${this.profileServiceUrl}/`; + const response = await fetch(`${this.getProfileServiceUrl()}pin`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(profile) + }); + + if (!response.ok) { + throw new Error(`Failed to create profile. Status: ${response.status} ${response.statusText}. Body: ${await response.text()}`); + } + + const data = await response.json(); + return data.cid; + } + + async get(cid: string): Promise { + const profileServiceUrl = this.profileServiceUrl.endsWith('/') ? this.profileServiceUrl : `${this.profileServiceUrl}/`; + const response = await fetch(`${this.getProfileServiceUrl()}get?cid=${cid}`); + if (!response.ok) { + throw new Error(`Failed to retrieve profile ${cid}. Status: ${response.status} ${response.statusText}. Body: ${await response.text()}`); + } + + return await response.json(); + } +} \ No newline at end of file diff --git a/packages/profiles/tsconfig.json b/packages/profiles/tsconfig.json new file mode 100644 index 0000000..d720b78 --- /dev/null +++ b/packages/profiles/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src"] +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 50baa7e..f0d57d9 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/sdk", - "version": "0.3.3", + "version": "0.4.0-profiles-preview-1", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,9 +17,11 @@ "author": "", "license": "ISC", "dependencies": { - "@circles-sdk/abi-v1": "0.3.3", - "@circles-sdk/abi-v2": "0.3.3", - "@circles-sdk/data": "0.3.3", + "@circles-sdk/abi-v1": "0.4.0-profiles-preview-1", + "@circles-sdk/abi-v2": "0.4.0-profiles-preview-1", + "@circles-sdk/data": "0.4.0-profiles-preview-1", + "@circles-sdk/utils": "0.4.0-profiles-preview-1", + "@circles-sdk/profiles": "0.4.0-profiles-preview-1", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, diff --git a/packages/sdk/rollup.config.js b/packages/sdk/rollup.config.js index a6e1bed..bed4a47 100644 --- a/packages/sdk/rollup.config.js +++ b/packages/sdk/rollup.config.js @@ -16,5 +16,5 @@ export default { tsconfig: './tsconfig.json' }) ], - external: ['ethers', 'multihashes', '@circles-sdk/abi-v1', '@circles-sdk/abi-v2', '@circles-sdk/data'] + external: ['ethers', 'multihashes', '@circles-sdk/abi-v1', '@circles-sdk/abi-v2', '@circles-sdk/data', '@circles-sdk/utils'] }; diff --git a/packages/sdk/src/chainConfig.ts b/packages/sdk/src/circlesConfig.ts similarity index 75% rename from packages/sdk/src/chainConfig.ts rename to packages/sdk/src/circlesConfig.ts index c30b270..bbaeb9b 100644 --- a/packages/sdk/src/chainConfig.ts +++ b/packages/sdk/src/circlesConfig.ts @@ -1,6 +1,7 @@ -export interface ChainConfig { +export interface CirclesConfig { readonly pathfinderUrl?: string; readonly circlesRpcUrl: string; + readonly profileServiceUrl?: string; readonly v1HubAddress: string; readonly v2HubAddress?: string; readonly nameRegistryAddress?: string; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 33369c7..73a96bd 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -2,7 +2,7 @@ export { Avatar } from './avatar'; export { Observable } from '@circles-sdk/data'; export { Sdk, SdkContractRunner } from './sdk'; export { V1Person } from './v1/v1Person'; -export { ChainConfig } from './chainConfig'; +export { CirclesConfig } from './circlesConfig'; export { AvatarRow, TrustListRow, TrustRelationRow } from '@circles-sdk/data'; export { AvatarInterface } from './AvatarInterface'; export { parseError } from './errors'; diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/sdk.ts index 624ec53..ec2b048 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/sdk.ts @@ -1,6 +1,6 @@ import { Avatar } from './avatar'; import { ContractRunner } from 'ethers'; -import { ChainConfig } from './chainConfig'; +import { CirclesConfig } from './circlesConfig'; import { Pathfinder } from './v1/pathfinder'; import { AvatarInterface } from './AvatarInterface'; import { Hub as HubV1, Hub__factory as HubV1Factory, Token__factory } from '@circles-sdk/abi-v1'; @@ -10,8 +10,9 @@ import { Migration__factory, NameRegistry, NameRegistry__factory } from '@circles-sdk/abi-v2'; import { AvatarRow, CirclesData, CirclesRpc } from '@circles-sdk/data'; -import multihashes from 'multihashes'; import { V1Person } from './v1/v1Person'; +import { cidV0ToUint8Array } from '@circles-sdk/utils'; +import { GroupProfile, Profile, Profiles } from '@circles-sdk/profiles'; /** * The SDK interface. @@ -24,7 +25,7 @@ interface SdkInterface { /** * The chain specific Circles configuration (contract addresses and rpc endpoints). */ - chainConfig: ChainConfig; + circlesConfig: CirclesConfig; /** * A configured instance of the CirclesData class, an easy-to-use wrapper around * the Circles RPC Query API. @@ -42,6 +43,10 @@ interface SdkInterface { * An instance of the v1 Pathfinder client (necessary for transfers; only available on gnosis chain with v1 Circles at the moment). */ v1Pathfinder?: Pathfinder; + /** + * Stores and retrieves profiles from the Circles profile service. + */ + profiles?: Profiles; /** * Gets an Avatar instance by its address. Fails if the avatar is not signed up at Circles. * @param avatarAddress The avatar's address. @@ -57,32 +62,29 @@ interface SdkInterface { * Registers the connected wallet as a human avatar in Circles v2. * @param cidV0 The CIDv0 of the avatar's ERC1155 token metadata. */ - registerHumanV2: (cidV0: string) => Promise; + registerHumanV2: (profile: Profile) => Promise; /** * Registers the connected wallet as an organization avatar in Circles v1. */ registerOrganization: () => Promise; /** * Registers the connected wallet as an organization avatar in Circles v2. - * @param name The organization's name. - * @param cidV0 The CIDv0 of the organization's metadata. + * @param profile The profile data of the organization. */ - registerOrganizationV2: (name: string, cidV0: string) => Promise; + registerOrganizationV2: (profile: Profile) => Promise; /** * Registers the connected wallet as a group avatar in Circles v2. * @param mint The address of the minting policy contract to use. - * @param name The group's name. - * @param symbol The group token's symbol. - * @param cidV0 The CIDv0 of the group token's metadata. + * @param profile The profile data of the group. */ - registerGroupV2: (mint: string, name: string, symbol: string, cidV0: string) => Promise; + registerGroupV2: (mint: string, profile: GroupProfile) => Promise; /** * Migrates a v1 avatar and all its Circles holdings to v2. * [[ Currently only works for human avatars. ]] * @param avatar The avatar's address. * @param cidV0 The CIDv0 of the avatar's ERC1155 token metadata. */ - migrateAvatar: (avatar: string, cidV0: string) => Promise; + migrateAvatar: (avatar: string, profile: Profile) => Promise; } /** @@ -110,7 +112,7 @@ export class Sdk implements SdkInterface { /** * The chain specific Circles configuration. */ - readonly chainConfig: ChainConfig; + readonly circlesConfig: CirclesConfig; /** * The Circles RPC client. */ @@ -135,27 +137,34 @@ export class Sdk implements SdkInterface { * The pathfinder client. */ readonly v1Pathfinder?: Pathfinder; + /** + * The profiles service client. + */ + readonly profiles?: Profiles; /** * Creates a new SDK instance. - * @param chainConfig The chain specific Circles configuration. + * @param circlesConfig The chain specific Circles configuration. * @param contractRunner A contract runner instance and its address. */ - constructor(chainConfig: ChainConfig, contractRunner: SdkContractRunner) { - this.chainConfig = chainConfig; + constructor(circlesConfig: CirclesConfig, contractRunner: SdkContractRunner) { + this.circlesConfig = circlesConfig; this.contractRunner = contractRunner; - this.circlesRpc = new CirclesRpc(chainConfig.circlesRpcUrl); + this.circlesRpc = new CirclesRpc(circlesConfig.circlesRpcUrl); this.data = new CirclesData(this.circlesRpc); - this.v1Hub = HubV1Factory.connect(chainConfig.v1HubAddress ?? '0x29b9a7fBb8995b2423a71cC17cf9810798F6C543', this.contractRunner.runner); - if (chainConfig.v2HubAddress) { - this.v2Hub = HubV2Factory.connect(chainConfig.v2HubAddress, this.contractRunner.runner); + this.v1Hub = HubV1Factory.connect(circlesConfig.v1HubAddress ?? '0x29b9a7fBb8995b2423a71cC17cf9810798F6C543', this.contractRunner.runner); + if (circlesConfig.v2HubAddress) { + this.v2Hub = HubV2Factory.connect(circlesConfig.v2HubAddress, this.contractRunner.runner); + } + if (circlesConfig.pathfinderUrl) { + this.v1Pathfinder = new Pathfinder(circlesConfig.pathfinderUrl); } - if (chainConfig.pathfinderUrl) { - this.v1Pathfinder = new Pathfinder(chainConfig.pathfinderUrl); + if (circlesConfig.nameRegistryAddress) { + this.nameRegistry = NameRegistry__factory.connect(circlesConfig.nameRegistryAddress, this.contractRunner.runner); } - if (chainConfig.nameRegistryAddress) { - this.nameRegistry = NameRegistry__factory.connect(chainConfig.nameRegistryAddress, this.contractRunner.runner); + if (circlesConfig.profileServiceUrl) { + this.profiles = new Profiles(circlesConfig.profileServiceUrl); } } @@ -186,20 +195,19 @@ export class Sdk implements SdkInterface { return this.getAvatar(signerAddress); }; - cidV0Digest = (cidV0: string) => { - if (!cidV0.startsWith('Qm')) { - throw new Error('Invalid CID. Must be a CIDv0 with sha2-256 hash in base58 encoding'); - } - const cidBytes = multihashes.fromB58String(cidV0); - const decodedCid = multihashes.decode(cidBytes); - return decodedCid.digest; - }; - - registerHumanV2 = async (cidV0: string): Promise => { + /** + * Registers the connected wallet as a human avatar in Circles v2. + * Note: This will only work if you already have a v1 avatar and only during the migration period. + * The only way to join after the migration period is to be invited by an existing member. + * @param profile The profile data of the avatar. + */ + registerHumanV2 = async (profile: Profile | string): Promise => { if (!this.v2Hub) { throw new Error('V2 hub not available'); } - const metadataDigest = this.cidV0Digest(cidV0); + + let metadataDigest: Uint8Array = await this.createProfileIfNecessary(profile); + const tx = await this.v2Hub.registerHuman(metadataDigest); const receipt = await tx.wait(); if (!receipt) { @@ -212,6 +220,26 @@ export class Sdk implements SdkInterface { return this.getAvatar(signerAddress); }; + /** + * Checks if the profile argument is a string or a Profile object and creates the profile if necessary. + * If the profile is a string, it must be a CIDv0 string (Qm...). + * @param profile The profile data or CIDv0 of the avatar. + * @private + */ + private async createProfileIfNecessary(profile: Profile | string) { + if (typeof profile === 'string') { + if (!profile.startsWith('Qm')) { + throw new Error('Invalid profile CID. Must be a CIDv0 string (Qm...).'); + } + return cidV0ToUint8Array(profile); + } else if (this.profiles) { + const profileCid = await this.profiles?.create(profile); + return cidV0ToUint8Array(profileCid); + } else { + throw new Error('Profiles service is not configured'); + } + } + /** * Registers the connected wallet as an organization avatar. * @returns The avatar instance. @@ -226,12 +254,17 @@ export class Sdk implements SdkInterface { return this.getAvatar(signerAddress); }; - registerOrganizationV2 = async (name: string, cidV0: string): Promise => { + /** + * Registers the connected wallet as an organization avatar in Circles v2. + * @param profile The profile data of the organization. + */ + registerOrganizationV2 = async (profile: Profile): Promise => { if (!this.v2Hub) { throw new Error('V2 hub not available'); } - const metadataDigest = this.cidV0Digest(cidV0); - const receipt = await this.v2Hub.registerOrganization(name, metadataDigest); + + const metadataDigest = await this.createProfileIfNecessary(profile); + const receipt = await this.v2Hub.registerOrganization(profile.name, metadataDigest); await receipt.wait(); const signerAddress = this.contractRunner.address; @@ -240,12 +273,18 @@ export class Sdk implements SdkInterface { return this.getAvatar(signerAddress); }; - registerGroupV2 = async (mint: string, name: string, symbol: string, cidV0: string): Promise => { + /** + * Registers the connected wallet as a group avatar in Circles v2. + * @param mint The address of the minting policy contract to use. + * @param profile The profile data of the group. + */ + registerGroupV2 = async (mint: string, profile: GroupProfile): Promise => { if (!this.v2Hub) { throw new Error('V2 hub not available'); } - const metatdataDigest = this.cidV0Digest(cidV0); - const receipt = await this.v2Hub.registerGroup(mint, name, symbol, metatdataDigest); + + const metadataDigest = await this.createProfileIfNecessary(profile); + const receipt = await this.v2Hub.registerGroup(mint, profile.name, profile.symbol, metadataDigest); await receipt.wait(); const signerAddress = this.contractRunner.address; @@ -270,7 +309,12 @@ export class Sdk implements SdkInterface { return avatarRow; }; - migrateAvatar = async (avatar: string, cidV0: string): Promise => { + /** + * Migrates a v1 avatar and all its Circles holdings to v2. + * @param avatar The avatar's address. + * @param profile The profile data of the avatar. + */ + migrateAvatar = async (avatar: string, profile: Profile): Promise => { if (!this.v2Hub) { throw new Error('V2 hub not available'); } @@ -297,7 +341,7 @@ export class Sdk implements SdkInterface { // 2. Signup V2 avatar if necessary if (avatarInfo.version === 1) { - await this.registerHumanV2(cidV0); + await this.registerHumanV2(profile); } // 3. Make sure the v1 token minting status is known to the v2 hub @@ -311,8 +355,12 @@ export class Sdk implements SdkInterface { } }; + /** + * Migrates all V1 tokens of an avatar to V2. + * @param avatar The avatar's address. + */ migrateAllV1Tokens = async (avatar: string): Promise => { - if (!this.chainConfig.migrationAddress) { + if (!this.circlesConfig.migrationAddress) { throw new Error('Migration address not set'); } const balances = await this.data.getTokenBalances(avatar, false); @@ -323,15 +371,15 @@ export class Sdk implements SdkInterface { await Promise.all(tokensToMigrate.map(async (t, i) => { const balance = BigInt(t.balance); const token = Token__factory.connect(t.token, this.contractRunner.runner); - const allowance = await token.allowance(avatar, this.chainConfig.migrationAddress!); + const allowance = await token.allowance(avatar, this.circlesConfig.migrationAddress!); if (allowance < balance) { const increase = balance - allowance; - const tx = await token.increaseAllowance(this.chainConfig.migrationAddress!, increase); + const tx = await token.increaseAllowance(this.circlesConfig.migrationAddress!, increase); await tx.wait(); } })); - const migrationContract = Migration__factory.connect(this.chainConfig.migrationAddress, this.contractRunner.runner); + const migrationContract = Migration__factory.connect(this.circlesConfig.migrationAddress, this.contractRunner.runner); const migrateTx = await migrationContract.migrate( tokensToMigrate.map(o => o.tokenOwner) , tokensToMigrate.map(o => BigInt(o.balance))); diff --git a/packages/sdk/src/v2/v2Person.ts b/packages/sdk/src/v2/v2Person.ts index 00e8926..d55e19a 100644 --- a/packages/sdk/src/v2/v2Person.ts +++ b/packages/sdk/src/v2/v2Person.ts @@ -7,6 +7,7 @@ import { TransactionHistoryRow, TrustRelationRow } from '@circles-sdk/data'; +import { cidV0ToUint8Array } from '@circles-sdk/utils'; export type FlowEdge = { streamSinkId: bigint; @@ -39,8 +40,8 @@ export class V2Person implements AvatarInterfaceV2 { async updateMetadata(cid: string): Promise { this.throwIfNameRegistryIsNotAvailable(); - - const digest = this.sdk.cidV0Digest(cid); + + const digest = cidV0ToUint8Array(cid); const tx = await this.sdk.nameRegistry?.updateMetadataDigest(digest); const receipt = await tx?.wait(); if (!receipt) { @@ -234,7 +235,7 @@ export class V2Person implements AvatarInterfaceV2 { } private throwIfV2IsNotAvailable() { - if (!this.sdk.chainConfig.v2HubAddress) { + if (!this.sdk.circlesConfig.v2HubAddress) { throw new Error('V2 is not available'); } } diff --git a/packages/tests/package-lock.json b/packages/tests/package-lock.json index 0052096..472a3ed 100644 --- a/packages/tests/package-lock.json +++ b/packages/tests/package-lock.json @@ -28,7 +28,7 @@ }, "../abi-v1": { "name": "@circles-sdk/abi-v1", - "version": "0.3.3", + "version": "0.4.0-profiles-preview-1", "license": "ISC", "dependencies": { "ethers": "^6.11.1" @@ -39,7 +39,7 @@ }, "../abi-v2": { "name": "@circles-sdk/abi-v2", - "version": "0.3.3", + "version": "0.4.0-profiles-preview-1", "license": "ISC", "dependencies": { "ethers": "^6.11.1" @@ -84,10 +84,10 @@ }, "../data": { "name": "@circles-sdk/data", - "version": "0.3.3", + "version": "0.4.0-profiles-preview-1", "license": "ISC", "dependencies": { - "@circles-sdk/utils": "0.3.3" + "@circles-sdk/utils": "0.4.0-profiles-preview-1" }, "devDependencies": { "typescript": "^5.3.3" @@ -95,12 +95,12 @@ }, "../sdk": { "name": "@circles-sdk/sdk", - "version": "0.3.3", + "version": "0.4.0-profiles-preview-1", "license": "ISC", "dependencies": { - "@circles-sdk/abi-v1": "0.3.3", - "@circles-sdk/abi-v2": "0.3.3", - "@circles-sdk/data": "0.3.3", + "@circles-sdk/abi-v1": "0.4.0-profiles-preview-1", + "@circles-sdk/abi-v2": "0.4.0-profiles-preview-1", + "@circles-sdk/data": "0.4.0-profiles-preview-1", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, diff --git a/packages/tests/package.json b/packages/tests/package.json index d369a36..3d8100a 100644 --- a/packages/tests/package.json +++ b/packages/tests/package.json @@ -11,6 +11,7 @@ "@circles-sdk/abi-v2": "../abi-v2", "@circles-sdk/sdk": "../sdk", "@circles-sdk/data": "../data", + "@circles-sdk/utils": "../utils", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, diff --git a/packages/tests/test/data/circlesData.test.ts b/packages/tests/test/data/circlesData.test.ts index 145a42e..ed81066 100644 --- a/packages/tests/test/data/circlesData.test.ts +++ b/packages/tests/test/data/circlesData.test.ts @@ -5,7 +5,7 @@ import { CirclesRpc } from '@circles-sdk/data'; // - V1_HUB_ADDRESS=0xdbf22d4e8962db3b2f1d9ff55be728a887e47710 // - V2_HUB_ADDRESS=0xFFfbD3E62203B888bb8E09c1fcAcE58242674964 // - V2_NAME_REGISTRY_ADDRESS=0x0A1D308a39A6dF8972A972E586E4b4b3Dc73520f -const circlesRpc = `https://rpc.falkenstein.aboutcircles.com`; +const circlesRpc = `https://chiado-rpc.aboutcircles.com`; const rpc = new CirclesRpc(circlesRpc); describe('CirclesData', () => { @@ -141,4 +141,11 @@ describe('CirclesData', () => { // Wait for events await new Promise(resolve => setTimeout(resolve, 60000)); }); + + it('should get events for a given avatar in a block range', async () => { + const circlesData = new CirclesData(rpc); + + const events = await circlesData.getEvents('0x389522f8f44cd5cd835d510a17b5f65f74a46468', 9500000); + expect(events).toBeDefined(); + }); }); \ No newline at end of file diff --git a/packages/tests/test/sdk/v1/v1Avatar.test.ts b/packages/tests/test/sdk/v1/v1Avatar.test.ts index 76c6da1..7fba0cd 100644 --- a/packages/tests/test/sdk/v1/v1Avatar.test.ts +++ b/packages/tests/test/sdk/v1/v1Avatar.test.ts @@ -1,9 +1,9 @@ -import { ChainConfig, Sdk } from '@circles-sdk/sdk'; +import { CirclesConfig, Sdk, SdkContractRunner } from '@circles-sdk/sdk'; import { ethers } from 'ethers'; import { parseError } from '@circles-sdk/sdk'; describe('V1Avatar', () => { - const chainConfig: ChainConfig = { + const chainConfig: CirclesConfig = { migrationAddress: '0x0A1D308a39A6dF8972A972E586E4b4b3Dc73520f', circlesRpcUrl: 'https://chiado-rpc.aboutcircles.com', pathfinderUrl: 'https://pathfinder.aboutcircles.com', diff --git a/packages/tests/test/utils/utils.test.ts b/packages/tests/test/utils/utils.test.ts new file mode 100644 index 0000000..c5245ff --- /dev/null +++ b/packages/tests/test/utils/utils.test.ts @@ -0,0 +1,30 @@ +import { + cidV0ToUint8Array, + hexStringToUint8Array, uint8ArrayToCidV0, + uint8ArrayToHexString +} from '@circles-sdk/utils'; + +describe('utils', () => { + it('should convert a CidV0 to a hex string', () => { + const cidV0 = 'QmWYATU4cCT5gNcSJnyp5hS7sHBj4wTtYRdqg5WfecjJMH'; + const uint8Array = cidV0ToUint8Array(cidV0); + console.log(`uint8Array: ${uint8Array.length} bytes`); + expect(uint8Array.length).toBe(32); + console.log(`uint8Array: ${uint8Array}`); + const hexString = uint8ArrayToHexString(uint8Array); + expect(hexString).toBe('79d0852096e3630e74b7ac00a74a5a2a162dd3cd255e5c33855fce784dd26fdc'); + console.log(`hexString: ${hexString}`); + }); + + it('should convert a hex string to a CidV0', () => { + const hexString = '79d0852096e3630e74b7ac00a74a5a2a162dd3cd255e5c33855fce784dd26fdc'; + const uint8Array = hexStringToUint8Array(hexString); + console.log(`uint8Array: ${uint8Array.length} bytes`); + expect(uint8Array.length).toBe(32); + console.log(`uint8Array: ${uint8Array}`); + + const cidV0 = uint8ArrayToCidV0(uint8Array); + console.log(`cidV0: ${cidV0}`); + expect(cidV0).toBe('QmWYATU4cCT5gNcSJnyp5hS7sHBj4wTtYRdqg5WfecjJMH'); + }); +}); \ No newline at end of file diff --git a/packages/utils/package.json b/packages/utils/package.json index dee28d8..bb3fbdc 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/utils", - "version": "0.3.3", + "version": "0.4.0-profiles-preview-1", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,8 @@ "build": "rollup -c" }, "dependencies": { - "bignumber.js": "^9.1.2" + "bignumber.js": "^9.1.2", + "multihashes": "^4.0.3" }, "keywords": [], "author": "", diff --git a/packages/utils/rollup.config.js b/packages/utils/rollup.config.js index 9b52ba2..66b0690 100644 --- a/packages/utils/rollup.config.js +++ b/packages/utils/rollup.config.js @@ -16,5 +16,5 @@ export default { tsconfig: './tsconfig.json' }) ], -external: ['bignumber.js', 'ethers'] + external: ['bignumber.js', 'ethers', 'multihashes'] }; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 39a38ae..3d980ac 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,5 +1,6 @@ import { BigNumber } from 'bignumber.js'; import { ethers, parseEther } from 'ethers'; +import multihash from 'multihashes'; /** * Formats the token balance in time circles. @@ -65,3 +66,60 @@ export function tcToCrc(timestamp: Date, amount: number): bigint { const payoutAtTimestamp = getCrcPayoutAt(ts); return parseEther((amount / 24 * payoutAtTimestamp).toString()); } + +/** + * Converts a CIDv0 string to a UInt8Array, stripping the hashing algorithm identifier. + * @param {string} cidV0 - The CIDv0 string (e.g., Qm...). + * @returns {Uint8Array} - The resulting UInt8Array of the 32-byte hash digest. + */ +export function cidV0ToUint8Array(cidV0: string) { + // Decode the base58 CIDv0 string to a Multihash + const multihashBytes = multihash.fromB58String(cidV0); + + // Verify the multihash algorithm (should be SHA-256) + const decodedMultihash = multihash.decode(multihashBytes); + if (decodedMultihash.code !== multihash.names['sha2-256']) { + throw new Error('Unsupported hash algorithm. Only SHA-256 is supported for CIDv0.'); + } + + // Extract and return the 32-byte hash digest + return decodedMultihash.digest; +} + +/** + * Converts a 32-byte UInt8Array back to a CIDv0 string by adding the hashing algorithm identifier. + * @param {Uint8Array} uint8Array - The 32-byte hash digest. + * @returns {string} - The resulting CIDv0 string (e.g., Qm...). + */ +export function uint8ArrayToCidV0(uint8Array: Uint8Array) { + if (uint8Array.length !== 32) { + throw new Error('Invalid array length. Expected 32 bytes.'); + } + + // Recreate the Multihash (prefix with SHA-256 code and length) + const multihashBytes = multihash.encode(uint8Array, 'sha2-256'); + + // Encode the Multihash as a base58 CIDv0 string + return multihash.toB58String(multihashBytes); +} + +/** + * Converts a Uint8Array to a hex string. + * @param uint8Array - The Uint8Array to convert. + */ +export function uint8ArrayToHexString(uint8Array: Uint8Array) { + return Array.from(uint8Array).map(byte => byte.toString(16).padStart(2, '0')).join(''); +} + +/** + * Converts a hex string to a Uint8Array. + * @param {string} hexString - The hex string to convert. + * @returns {Uint8Array} - The resulting Uint8Array. + */ +export function hexStringToUint8Array(hexString: string) { + const bytes = []; + for (let i = 0; i < hexString.length; i += 2) { + bytes.push(parseInt(hexString.substr(i, 2), 16)); + } + return new Uint8Array(bytes); +} \ No newline at end of file From 98509b37e6adce19514b71ab065ef93f2eade85b Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:42:27 +0200 Subject: [PATCH 2/8] add "external" profiles package to sdk's rollup config --- packages/sdk/rollup.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sdk/rollup.config.js b/packages/sdk/rollup.config.js index bed4a47..3415db7 100644 --- a/packages/sdk/rollup.config.js +++ b/packages/sdk/rollup.config.js @@ -16,5 +16,5 @@ export default { tsconfig: './tsconfig.json' }) ], - external: ['ethers', 'multihashes', '@circles-sdk/abi-v1', '@circles-sdk/abi-v2', '@circles-sdk/data', '@circles-sdk/utils'] + external: ['ethers', 'multihashes', '@circles-sdk/abi-v1', '@circles-sdk/abi-v2', '@circles-sdk/data', '@circles-sdk/utils', '@circles-sdk/profiles'] }; From c007a0a770d4eea4be8bf3bf88b3ba5a7dff2ba9 Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Sat, 13 Jul 2024 23:08:00 +0200 Subject: [PATCH 3/8] merged 0.4.0 and set version to 0.5.0 --- package-lock.json | 51 ++++++++++++++++++++++---------- package.json | 5 ++-- packages/abi-v1/package.json | 2 +- packages/abi-v2/package.json | 2 +- packages/data/package.json | 4 +-- packages/profiles/package.json | 4 +-- packages/sdk/package.json | 9 +++--- packages/tests/package-lock.json | 18 +++++------ packages/utils/package.json | 2 +- 9 files changed, 60 insertions(+), 37 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2dd353a..6cd295a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,20 @@ { "name": "@cirlces-sdk/root", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cirlces-sdk/root", + "version": "0.5.0", "license": "MIT", "workspaces": [ "packages/abi-v1", "packages/abi-v2", "packages/sdk", "packages/data", - "packages/utils" + "packages/utils", + "packages/profiles" ], "devDependencies": { "@rollup/plugin-json": "^6.1.0", @@ -628,6 +631,10 @@ "resolved": "packages/data", "link": true }, + "node_modules/@circles-sdk/profiles": { + "resolved": "packages/profiles", + "link": true + }, "node_modules/@circles-sdk/sdk": { "resolved": "packages/sdk", "link": true @@ -1489,6 +1496,13 @@ "node-int64": "^0.4.0" } }, + "node_modules/bser/node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/buffer-from": { "version": "1.1.2", "dev": true, @@ -3218,11 +3232,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-int64": { - "version": "0.4.0", - "dev": true, - "license": "MIT" - }, "node_modules/node-releases": { "version": "2.0.14", "dev": true, @@ -4204,7 +4213,7 @@ }, "packages/abi-v1": { "name": "@circles-sdk/abi-v1", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "dependencies": { "ethers": "^6.11.1" @@ -4215,7 +4224,7 @@ }, "packages/abi-v2": { "name": "@circles-sdk/abi-v2", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "dependencies": { "ethers": "^6.11.1" @@ -4226,10 +4235,21 @@ }, "packages/data": { "name": "@circles-sdk/data", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "dependencies": { - "@circles-sdk/utils": "0.4.0" + "@circles-sdk/utils": "0.5.0" + }, + "devDependencies": { + "typescript": "^5.3.3" + } + }, + "packages/profiles": { + "name": "@circles-sdk/profiles", + "version": "0.5.0", + "license": "ISC", + "dependencies": { + "@circles-sdk/utils": "0.5.0" }, "devDependencies": { "typescript": "^5.3.3" @@ -4237,12 +4257,13 @@ }, "packages/sdk": { "name": "@circles-sdk/sdk", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "dependencies": { - "@circles-sdk/abi-v1": "0.4.0", - "@circles-sdk/abi-v2": "0.4.0", - "@circles-sdk/data": "0.4.0", + "@circles-sdk/abi-v1": "0.5.0", + "@circles-sdk/abi-v2": "0.5.0", + "@circles-sdk/data": "0.5.0", + "@circles-sdk/profiles": "0.5.0", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, @@ -4252,7 +4273,7 @@ }, "packages/utils": { "name": "@circles-sdk/utils", - "version": "0.4.0", + "version": "0.5.0", "license": "MIT", "dependencies": { "bignumber.js": "^9.1.2" diff --git a/package.json b/package.json index fe26161..63c5fcb 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "packages/abi-v2", "packages/sdk", "packages/data", - "packages/utils" + "packages/utils", + "packages/profiles" ], "scripts": { "test": "jest", @@ -30,5 +31,5 @@ }, "name": "@cirlces-sdk/root", "license": "MIT", - "version": "0.4.0" + "version": "0.5.0" } diff --git a/packages/abi-v1/package.json b/packages/abi-v1/package.json index 0293884..361df43 100644 --- a/packages/abi-v1/package.json +++ b/packages/abi-v1/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/abi-v1", - "version": "0.4.0", + "version": "0.5.0", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/abi-v2/package.json b/packages/abi-v2/package.json index 0954609..0de32c1 100644 --- a/packages/abi-v2/package.json +++ b/packages/abi-v2/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/abi-v2", - "version": "0.4.0", + "version": "0.5.0", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/data/package.json b/packages/data/package.json index 527f61a..9eb7f88 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/data", - "version": "0.4.0", + "version": "0.5.0", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/utils": "0.4.0" + "@circles-sdk/utils": "0.5.0" }, "keywords": [], "author": "", diff --git a/packages/profiles/package.json b/packages/profiles/package.json index 1852b89..66209b8 100644 --- a/packages/profiles/package.json +++ b/packages/profiles/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/profiles", - "version": "0.4.0-profiles-preview-1", + "version": "0.5.0", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/utils": "0.4.0-profiles-preview-1" + "@circles-sdk/utils": "0.5.0" }, "keywords": [], "author": "", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a161a73..6143146 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/sdk", - "version": "0.4.0", + "version": "0.5.0", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,9 +17,10 @@ "author": "", "license": "MIT", "dependencies": { - "@circles-sdk/abi-v1": "0.4.0", - "@circles-sdk/abi-v2": "0.4.0", - "@circles-sdk/data": "0.4.0", + "@circles-sdk/abi-v1": "0.5.0", + "@circles-sdk/abi-v2": "0.5.0", + "@circles-sdk/data": "0.5.0", + "@circles-sdk/profiles": "0.5.0", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, diff --git a/packages/tests/package-lock.json b/packages/tests/package-lock.json index 92318cb..4f08a49 100644 --- a/packages/tests/package-lock.json +++ b/packages/tests/package-lock.json @@ -28,7 +28,7 @@ }, "../abi-v1": { "name": "@circles-sdk/abi-v1", - "version": "0.4.0", + "version": "0.5.0", "license": "ISC", "dependencies": { "ethers": "^6.11.1" @@ -39,7 +39,7 @@ }, "../abi-v2": { "name": "@circles-sdk/abi-v2", - "version": "0.4.0", + "version": "0.5.0", "license": "ISC", "dependencies": { "ethers": "^6.11.1" @@ -84,10 +84,10 @@ }, "../data": { "name": "@circles-sdk/data", - "version": "0.4.0", + "version": "0.5.0", "license": "ISC", "dependencies": { - "@circles-sdk/utils": "0.4.0" + "@circles-sdk/utils": "0.5.0" }, "devDependencies": { "typescript": "^5.3.3" @@ -95,12 +95,12 @@ }, "../sdk": { "name": "@circles-sdk/sdk", - "version": "0.4.0", + "version": "0.5.0", "license": "ISC", "dependencies": { - "@circles-sdk/abi-v1": "0.4.0", - "@circles-sdk/abi-v2": "0.4.0", - "@circles-sdk/data": "0.4.0", + "@circles-sdk/abi-v1": "0.5.0", + "@circles-sdk/abi-v2": "0.5.0", + "@circles-sdk/data": "0.5.0", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, @@ -5817,7 +5817,7 @@ "dev": true }, "node_modules/node-int64": { - "version": "0.4.0", + "version": "0.5.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true diff --git a/packages/utils/package.json b/packages/utils/package.json index bf33017..aeaaf3a 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/utils", - "version": "0.4.0", + "version": "0.5.0", "description": "", "type": "module", "main": "./dist/index.js", From 5d397eeb26ebee1f9e9fe5b7534e4b1b6560ffab Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:47:00 +0200 Subject: [PATCH 4/8] add the profiles package to the build script --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 63c5fcb..f0acaa5 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ ], "scripts": { "test": "jest", - "build": "./generateCode.sh && npm run build -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/utils -w @circles-sdk/data -w @circles-sdk/sdk", - "publish": "npm run build && npm publish -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/data -w @circles-sdk/utils -w @circles-sdk/sdk", - "publish:dry-run": "npm run build && npm publish --dry-run -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/data -w @circles-sdk/sdk -w @circles-sdk/utils" + "build": "./generateCode.sh && npm run build -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/utils -w @circles-sdk/data -w @circles-sdk/profiles -w @circles-sdk/sdk", + "publish": "npm run build && npm publish -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/utils -w @circles-sdk/data -w @circles-sdk/profiles -w @circles-sdk/sdk", + "publish:dry-run": "npm run build && npm publish --dry-run -w @circles-sdk/abi-v1 -w @circles-sdk/abi-v2 -w @circles-sdk/utils -w @circles-sdk/data -w @circles-sdk/profiles -w @circles-sdk/sdk" }, "devDependencies": { "@rollup/plugin-json": "^6.1.0", From fe6d94637695d22d5f410a6cade6992ac3cebcd6 Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Thu, 18 Jul 2024 19:41:04 +0200 Subject: [PATCH 5/8] * rename v1/v2Person -> v1/v2Avatar * fix v2 transfer: give allowance to v2hub instead of sender * add util functions to convert between v2 token id and address --- packages/sdk/src/avatar.ts | 8 +-- packages/sdk/src/index.ts | 2 +- packages/sdk/src/sdk.ts | 4 +- .../sdk/src/v1/{v1Person.ts => v1Avatar.ts} | 2 +- .../sdk/src/v2/{v2Person.ts => v2Avatar.ts} | 49 +++++++++++++++++-- packages/tests/package-lock.json | 32 +++++++----- packages/tests/test/sdk/v1/v1Avatar.test.ts | 10 +--- packages/utils/src/index.ts | 29 +++++++++++ 8 files changed, 102 insertions(+), 34 deletions(-) rename packages/sdk/src/v1/{v1Person.ts => v1Avatar.ts} (99%) rename packages/sdk/src/v2/{v2Person.ts => v2Avatar.ts} (84%) diff --git a/packages/sdk/src/avatar.ts b/packages/sdk/src/avatar.ts index 36bb431..aa87cf1 100644 --- a/packages/sdk/src/avatar.ts +++ b/packages/sdk/src/avatar.ts @@ -1,4 +1,4 @@ -import { V1Person } from './v1/v1Person'; +import { V1Avatar } from './v1/v1Avatar'; import { ContractTransactionReceipt, parseEther } from 'ethers'; import { Sdk } from './sdk'; import { AvatarInterface, AvatarInterfaceV2 } from './AvatarInterface'; @@ -8,7 +8,7 @@ import { TransactionHistoryRow, TrustRelationRow } from '@circles-sdk/data'; -import { V2Person } from './v2/v2Person'; +import { V2Avatar } from './v2/v2Avatar'; import { CirclesEvent } from '@circles-sdk/data'; import { tcToCrc } from '@circles-sdk/utils'; @@ -73,8 +73,8 @@ export class Avatar implements AvatarInterfaceV2 { } const { version, hasV1 } = this._avatarInfo; - const v1Person = () => new V1Person(this._sdk, this._avatarInfo!); - const v2Person = () => new V2Person(this._sdk, this._avatarInfo!); + const v1Person = () => new V1Avatar(this._sdk, this._avatarInfo!); + const v2Person = () => new V2Avatar(this._sdk, this._avatarInfo!); switch (version) { case 1: diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 73a96bd..c89a1ae 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,7 +1,7 @@ export { Avatar } from './avatar'; export { Observable } from '@circles-sdk/data'; export { Sdk, SdkContractRunner } from './sdk'; -export { V1Person } from './v1/v1Person'; +export { V1Avatar } from './v1/v1Avatar'; export { CirclesConfig } from './circlesConfig'; export { AvatarRow, TrustListRow, TrustRelationRow } from '@circles-sdk/data'; export { AvatarInterface } from './AvatarInterface'; diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/sdk.ts index 88a95cf..c9b4aae 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/sdk.ts @@ -10,7 +10,7 @@ import { Migration__factory, NameRegistry, NameRegistry__factory } from '@circles-sdk/abi-v2'; import { AvatarRow, CirclesData, CirclesRpc } from '@circles-sdk/data'; -import { V1Person } from './v1/v1Person'; +import { V1Avatar } from './v1/v1Avatar'; import { cidV0ToUint8Array } from '@circles-sdk/utils'; import { GroupProfile, Profile, Profiles } from '@circles-sdk/profiles'; @@ -326,7 +326,7 @@ export class Sdk implements SdkInterface { if (avatarInfo.hasV1) { // 1. Stop V1 token if necessary if (avatarInfo.v1Token) { - const v1Avatar = new V1Person(this, avatarInfo); + const v1Avatar = new V1Avatar(this, avatarInfo); const isStopped = await v1Avatar.v1Token?.stopped(); if (!isStopped) { diff --git a/packages/sdk/src/v1/v1Person.ts b/packages/sdk/src/v1/v1Avatar.ts similarity index 99% rename from packages/sdk/src/v1/v1Person.ts rename to packages/sdk/src/v1/v1Avatar.ts index 2ea8e40..4990e1f 100644 --- a/packages/sdk/src/v1/v1Person.ts +++ b/packages/sdk/src/v1/v1Avatar.ts @@ -12,7 +12,7 @@ import { } from '@circles-sdk/data'; import { crcToTc } from '@circles-sdk/utils'; -export class V1Person implements AvatarInterface { +export class V1Avatar implements AvatarInterface { public readonly sdk: Sdk; get address(): string { diff --git a/packages/sdk/src/v2/v2Person.ts b/packages/sdk/src/v2/v2Avatar.ts similarity index 84% rename from packages/sdk/src/v2/v2Person.ts rename to packages/sdk/src/v2/v2Avatar.ts index 3fcd9a7..bd224b2 100644 --- a/packages/sdk/src/v2/v2Person.ts +++ b/packages/sdk/src/v2/v2Avatar.ts @@ -1,5 +1,8 @@ import { AvatarInterfaceV2 } from '../AvatarInterface'; -import { ContractTransactionReceipt, formatEther } from 'ethers'; +import { + ContractTransactionReceipt, + formatEther +} from 'ethers'; import { Sdk } from '../sdk'; import { AvatarRow, @@ -7,7 +10,7 @@ import { TransactionHistoryRow, TrustRelationRow } from '@circles-sdk/data'; -import { cidV0ToUint8Array } from '@circles-sdk/utils'; +import { addressToUInt256, cidV0ToUint8Array } from '@circles-sdk/utils'; export type FlowEdge = { streamSinkId: bigint; @@ -20,7 +23,7 @@ export type Stream = { data: Uint8Array } -export class V2Person implements AvatarInterfaceV2 { +export class V2Avatar implements AvatarInterfaceV2 { public readonly sdk: Sdk; get address(): string { @@ -116,8 +119,10 @@ export class V2Person implements AvatarInterfaceV2 { return { sortedAddresses, lookupMap }; } - async transfer(to: string, amount: bigint, token?: string): Promise { + private async transitiveTransfer(to: string, amount: bigint): Promise { this.throwIfV2IsNotAvailable(); + + const addresses = [this.address, to]; const N = addresses.length; @@ -166,7 +171,11 @@ export class V2Person implements AvatarInterfaceV2 { const approvalStatus = await this.sdk.v2Hub!.isApprovedForAll(this.address, to); if (!approvalStatus) { - await this.sdk.v2Hub!.setApprovalForAll(this.address, true); + const v2HubAddress = await this.sdk.v2Hub?.getAddress(); + if (!v2HubAddress) { + throw new Error('V2 hub address not found'); + } + await this.sdk.v2Hub!.setApprovalForAll(v2HubAddress, true); } const tx = await this.sdk.v2Hub!.operateFlowMatrix(flowVertices, flow, streams, packedCoordinates); @@ -178,6 +187,36 @@ export class V2Person implements AvatarInterfaceV2 { return receipt; } + private async directTransfer(to: string, amount: bigint, tokenAddress: string): Promise { + const tokenInf = await this.sdk.data.getTokenInfo(tokenAddress); + if (!tokenInf) { + throw new Error('Token not found'); + } + + const numericTokenId = addressToUInt256(tokenInf.tokenId); + const tx = await this.sdk.v2Hub?.safeTransferFrom( + this.address, + to, + numericTokenId, + amount, + new Uint8Array(0)); + + const receipt = await tx?.wait(); + if (!receipt) { + throw new Error('Transfer failed'); + } + + return receipt; + } + + async transfer(to: string, amount: bigint, tokenAddress?: string): Promise { + if (!tokenAddress) { + return this.transitiveTransfer(to, amount); + } else { + return this.directTransfer(to, amount, tokenAddress); + } + } + async trust(avatar: string): Promise { this.throwIfV2IsNotAvailable(); const tx = await this.sdk.v2Hub!.trust(avatar, BigInt('79228162514264337593543950335')); diff --git a/packages/tests/package-lock.json b/packages/tests/package-lock.json index 4f08a49..c06cc10 100644 --- a/packages/tests/package-lock.json +++ b/packages/tests/package-lock.json @@ -12,8 +12,7 @@ "@circles-sdk/abi-v2": "../abi-v2", "@circles-sdk/data": "../data", "@circles-sdk/sdk": "../sdk", - "@circles/circles-contracts": "../circles-contracts", - "@circles/circles-contracts-v2": "../circles-contracts-v2", + "@circles-sdk/utils": "../utils", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, @@ -29,7 +28,7 @@ "../abi-v1": { "name": "@circles-sdk/abi-v1", "version": "0.5.0", - "license": "ISC", + "license": "MIT", "dependencies": { "ethers": "^6.11.1" }, @@ -40,7 +39,7 @@ "../abi-v2": { "name": "@circles-sdk/abi-v2", "version": "0.5.0", - "license": "ISC", + "license": "MIT", "dependencies": { "ethers": "^6.11.1" }, @@ -51,6 +50,7 @@ "../circles-contracts": { "name": "@circles/circles-contracts", "version": "3.3.2", + "extraneous": true, "license": "AGPL-3.0", "dependencies": { "@babel/core": "^7.21.8", @@ -79,13 +79,14 @@ "../circles-contracts-v2": { "name": "@circles/circles-contracts-v2", "version": "0.2.1", + "extraneous": true, "license": "AGPL-3.0", "devDependencies": {} }, "../data": { "name": "@circles-sdk/data", "version": "0.5.0", - "license": "ISC", + "license": "MIT", "dependencies": { "@circles-sdk/utils": "0.5.0" }, @@ -96,11 +97,12 @@ "../sdk": { "name": "@circles-sdk/sdk", "version": "0.5.0", - "license": "ISC", + "license": "MIT", "dependencies": { "@circles-sdk/abi-v1": "0.5.0", "@circles-sdk/abi-v2": "0.5.0", "@circles-sdk/data": "0.5.0", + "@circles-sdk/profiles": "0.5.0", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, @@ -108,6 +110,16 @@ "typescript": "^5.3.3" } }, + "../utils": { + "version": "0.5.0", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.1.2" + }, + "devDependencies": { + "typescript": "^5.3.3" + } + }, "node_modules/@adraffy/ens-normalize": { "version": "1.10.1", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", @@ -1881,12 +1893,8 @@ "resolved": "../sdk", "link": true }, - "node_modules/@circles/circles-contracts": { - "resolved": "../circles-contracts", - "link": true - }, - "node_modules/@circles/circles-contracts-v2": { - "resolved": "../circles-contracts-v2", + "node_modules/@circles-sdk/utils": { + "resolved": "../utils", "link": true }, "node_modules/@istanbuljs/load-nyc-config": { diff --git a/packages/tests/test/sdk/v1/v1Avatar.test.ts b/packages/tests/test/sdk/v1/v1Avatar.test.ts index 7fba0cd..d137e4d 100644 --- a/packages/tests/test/sdk/v1/v1Avatar.test.ts +++ b/packages/tests/test/sdk/v1/v1Avatar.test.ts @@ -14,18 +14,10 @@ describe('V1Avatar', () => { describe('initialize', () => { it('should initialize the avatar', async () => { - const error = '0x03dee4c500000000000000000000000062f1e5d9d635cda3b61d1397f41f465e2fe37a67000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005f2d30a88a3347400000000000000000000000062f1e5d9d635cda3b61d1397f41f465e2fe37a67'; + const error = '0xde84eec90000000000000000000000002dadafd4dcb8ac187a90e04eeadf614de69dee73000000000000000000000000d68193591d47740e51dfbc410da607a351b565860000000000000000000000000000000000000000000000000000000000000001'; const decoded = parseError(error); console.log(decoded); - const wallet = ethers.Wallet.createRandom(); - const sdk = new Sdk(chainConfig, { - runner: wallet, - address: wallet.address - }); - const avatar = await sdk.getAvatar('0xD68193591d47740E51dFBc410da607A351b56586'); - const trustRelations = await avatar.getTrustRelations(); - console.log(trustRelations); }); }); }); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 59adfb4..5bdf059 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -125,4 +125,33 @@ export function hexStringToUint8Array(hexString: string) { bytes.push(parseInt(hexString.substr(i, 2), 16)); } return new Uint8Array(bytes); +} + +export function addressToUInt256(address: string): bigint { + // Remove the '0x' prefix if it exists + if (address.startsWith('0x')) { + address = address.slice(2); + } + + // Convert the address to a BigInt + const addressBigInt = BigInt('0x' + address); + + // Shift the address left by 96 bits (256 - 160 bits) + const uint256BigInt = addressBigInt << BigInt(96); + + return uint256BigInt; +} + +export function uint256ToAddress(uint256: bigint): string { + // Right shift by 96 bits to remove the padding + const addressBigInt = uint256 >> BigInt(96); + + // Convert the BigInt to a hexadecimal string + let addressHex = addressBigInt.toString(16); + + // Ensure the address is 40 characters long (160 bits / 4 bits per hex digit) + addressHex = addressHex.padStart(40, '0'); + + // Add the '0x' prefix + return '0x' + addressHex; } \ No newline at end of file From 37276133f98cf78b4484c93f24ce974ee18e8f73 Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Fri, 19 Jul 2024 09:13:43 +0200 Subject: [PATCH 6/8] * add data.getAvatarInfos * Use V_CrcV2_Groups view in data.findGroups * Add cidV0Digest to GroupRow --- packages/data/src/circlesData.ts | 58 ++++++++++----- packages/data/src/rows/groupRow.ts | 1 + packages/data/src/rpcSchema/namespaces.ts | 2 +- packages/profiles/src/index.ts | 90 +++++++++++++++-------- 4 files changed, 99 insertions(+), 52 deletions(-) diff --git a/packages/data/src/circlesData.ts b/packages/data/src/circlesData.ts index 47ce7da..ebc4e39 100644 --- a/packages/data/src/circlesData.ts +++ b/packages/data/src/circlesData.ts @@ -245,8 +245,23 @@ export class CirclesData implements CirclesDataInterface { * Gets basic information about an avatar. * This includes the signup timestamp, circles version, avatar type and token address/id. * @param avatar The address to check. + * @returns The avatar info or undefined if the avatar is not found. */ async getAvatarInfo(avatar: string): Promise { + const avatarInfos = await this.getAvatarInfos([avatar]); + return avatarInfos.length > 0 ? avatarInfos[0] : undefined; + } + + /** + * Gets basic information about multiple avatars. + * @param avatars The addresses to check. + * @returns An array of avatar info objects. + */ + async getAvatarInfos(avatars: string[]): Promise { + if (avatars.length === 0) { + return []; + } + const circlesQuery = new CirclesQuery(this.rpc, { namespace: 'V_Crc', table: 'Avatars', @@ -265,14 +280,14 @@ export class CirclesData implements CirclesDataInterface { filter: [ { Type: 'FilterPredicate', - FilterType: 'Equals', + FilterType: 'In', Column: 'avatar', - Value: avatar.toLowerCase() + Value: avatars.map(a => a.toLowerCase()) } ], sortOrder: 'ASC', limit: 1000 - }, [{ + }, [{ name: 'cidV0', generator: async (row: AvatarRow) => { try { @@ -289,30 +304,34 @@ export class CirclesData implements CirclesDataInterface { } }]); - if (!await circlesQuery.queryNextPage()) { - return undefined; + const results: AvatarRow[] = []; + + while (await circlesQuery.queryNextPage()) { + const resultRows = circlesQuery.currentPage?.results ?? []; + if (resultRows.length === 0) break; + results.push(...resultRows); + if (resultRows.length < 1000) break; } - const result = circlesQuery.currentPage?.results ?? []; - let returnValue: AvatarRow | undefined = undefined; + const avatarMap: { [key: string]: AvatarRow } = {}; - for (const avatarRow of result) { - if (returnValue === undefined) { - returnValue = avatarRow; + results.forEach(avatarRow => { + if (!avatarMap[avatarRow.avatar]) { + avatarMap[avatarRow.avatar] = avatarRow; } if (avatarRow.version === 1) { - returnValue.hasV1 = true; - returnValue.v1Token = avatarRow.tokenId; + avatarMap[avatarRow.avatar].hasV1 = true; + avatarMap[avatarRow.avatar].v1Token = avatarRow.tokenId; } else { - returnValue = { - ...returnValue, + avatarMap[avatarRow.avatar] = { + ...avatarMap[avatarRow.avatar], ...avatarRow }; } - } + }); - return returnValue; + return avatars.map(avatar => avatarMap[avatar.toLowerCase()]).filter(row => row !== undefined); } /** @@ -443,8 +462,8 @@ export class CirclesData implements CirclesDataInterface { */ findGroups(pageSize: number, params?: GroupQueryParams): CirclesQuery { const queryDefintion: PagedQueryParams = { - namespace: 'CrcV2', - table: 'RegisterGroup', + namespace: 'V_CrcV2', + table: 'Groups', columns: [ 'blockNumber', 'timestamp', @@ -455,7 +474,8 @@ export class CirclesData implements CirclesDataInterface { 'mint', 'treasury', 'name', - 'symbol' + 'symbol', + 'cidV0Digest', ], sortOrder: 'DESC', limit: pageSize diff --git a/packages/data/src/rows/groupRow.ts b/packages/data/src/rows/groupRow.ts index 0707dfd..fb5bcd7 100644 --- a/packages/data/src/rows/groupRow.ts +++ b/packages/data/src/rows/groupRow.ts @@ -7,4 +7,5 @@ export interface GroupRow extends EventRow { name: string; symbol: string; isMember?: boolean; + cidV0Digest?: string; } \ No newline at end of file diff --git a/packages/data/src/rpcSchema/namespaces.ts b/packages/data/src/rpcSchema/namespaces.ts index 24690f2..2bd54cd 100644 --- a/packages/data/src/rpcSchema/namespaces.ts +++ b/packages/data/src/rpcSchema/namespaces.ts @@ -3,4 +3,4 @@ export type Table = V1Table | V_V1Table | V2Table | V_V2Table; export type V1Table = 'HubTransfer' | 'Trust' | 'Transfer' | 'Signup' | 'OrganizationSignup'; export type V_V1Table = 'Avatars' | 'TrustRelations'; export type V2Table = 'ApprovalForAll' | 'DiscountCost' | 'InviteHuman' | 'PersonalMint' | 'RegisterGroup' | 'RegisterHuman' | 'RegisterOrganization' | 'RegisterShortName' | 'Stopped' | 'TransferBatch' | 'TransferSingle' | 'Trust' | 'UpdateMetadataDigest' | 'URI'; -export type V_V2Table = 'Avatars' | 'TrustRelations' | 'Transfers' | 'GroupMemberships'; \ No newline at end of file +export type V_V2Table = 'Avatars' | 'TrustRelations' | 'Transfers' | 'GroupMemberships' | 'Groups'; \ No newline at end of file diff --git a/packages/profiles/src/index.ts b/packages/profiles/src/index.ts index 24421f9..be3b35d 100644 --- a/packages/profiles/src/index.ts +++ b/packages/profiles/src/index.ts @@ -1,45 +1,71 @@ export interface Profile { - name: string; - description?: string; - previewImageUrl?: string; - imageUrl?: string; + name: string; + description?: string; + previewImageUrl?: string; + imageUrl?: string; } export interface GroupProfile extends Profile { - symbol: string; + symbol: string; } export class Profiles { - constructor(private readonly profileServiceUrl: string) { - } - - private getProfileServiceUrl(): string { - return this.profileServiceUrl.endsWith('/') ? this.profileServiceUrl : `${this.profileServiceUrl}/`; - } - - async create(profile: Profile): Promise { - const profileServiceUrl = this.profileServiceUrl.endsWith('/') ? this.profileServiceUrl : `${this.profileServiceUrl}/`; - const response = await fetch(`${this.getProfileServiceUrl()}pin`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(profile) - }); - - if (!response.ok) { - throw new Error(`Failed to create profile. Status: ${response.status} ${response.statusText}. Body: ${await response.text()}`); + constructor(private readonly profileServiceUrl: string) { } - const data = await response.json(); - return data.cid; - } + private getProfileServiceUrl(): string { + return this.profileServiceUrl.endsWith('/') ? this.profileServiceUrl : `${this.profileServiceUrl}/`; + } + + async create(profile: Profile): Promise { + const response = await fetch(`${this.getProfileServiceUrl()}pin`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(profile) + }); + + if (!response.ok) { + throw new Error(`Failed to create profile. Status: ${response.status} ${response.statusText}. Body: ${await response.text()}`); + } + + const data = await response.json(); + return data.cid; + } - async get(cid: string): Promise { - const profileServiceUrl = this.profileServiceUrl.endsWith('/') ? this.profileServiceUrl : `${this.profileServiceUrl}/`; - const response = await fetch(`${this.getProfileServiceUrl()}get?cid=${cid}`); - if (!response.ok) { - throw new Error(`Failed to retrieve profile ${cid}. Status: ${response.status} ${response.statusText}. Body: ${await response.text()}`); + /** + * Retrieves a profile by its CID. If the profile is not found, an error is thrown. + * @param cid The CID of the profile to retrieve. + */ + async get(cid: string): Promise { + const response = await fetch(`${this.getProfileServiceUrl()}get?cid=${cid}`); + if (!response.ok) { + console.warn(`Failed to retrieve profile ${cid}. Status: ${response.status} ${response.statusText}. Body: ${await response.text()}`); + return undefined; + } + + return await response.json(); } - return await response.json(); - } + /** + * Retrieves multiple profiles by their CIDs. If a profile is not found, it will not be included in the result. + * @param cids The CIDs of the profiles to retrieve. + * @returns A map of CIDs to profiles. If a profile is not found, it will not be included in the map. + */ + async getMany(cids: string[]): Promise> { + const response = await fetch(`${this.getProfileServiceUrl()}getBatch?cids=${cids.join(',')}`); + if (!response.ok) { + throw new Error(`Failed to retrieve profiles ${cids.join(',')}. Status: ${response.status} ${response.statusText}. Body: ${await response.text()}`); + } + + const profilesArray = await response.json(); + const profiles: Record = {}; + + for (let i = 0; i < cids.length; i++) { + if (profilesArray[i]) { + profiles[cids[i]] = profilesArray[i]; + } + } + + return profiles; + } } \ No newline at end of file From 21e1eef453efd7a5c3fe4ac00122549406c74648 Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Mon, 22 Jul 2024 03:23:16 +0200 Subject: [PATCH 7/8] add v2 pathfinder (0.5.0-preview-1) --- package-lock.json | 28 ++++++++++++++-------------- package.json | 2 +- packages/abi-v1/package.json | 2 +- packages/abi-v2/package.json | 2 +- packages/data/package.json | 4 ++-- packages/profiles/package.json | 4 ++-- packages/sdk/package.json | 10 +++++----- packages/sdk/src/circlesConfig.ts | 1 + packages/sdk/src/sdk.ts | 9 ++++++++- packages/sdk/src/v2/v2Avatar.ts | 17 ++++++++++++++--- packages/tests/package-lock.json | 20 ++++++++++---------- packages/utils/package.json | 2 +- 12 files changed, 60 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6cd295a..6515269 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cirlces-sdk/root", - "version": "0.5.0", + "version": "0.5.0-preview-1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cirlces-sdk/root", - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "MIT", "workspaces": [ "packages/abi-v1", @@ -4213,7 +4213,7 @@ }, "packages/abi-v1": { "name": "@circles-sdk/abi-v1", - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "MIT", "dependencies": { "ethers": "^6.11.1" @@ -4224,7 +4224,7 @@ }, "packages/abi-v2": { "name": "@circles-sdk/abi-v2", - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "MIT", "dependencies": { "ethers": "^6.11.1" @@ -4235,10 +4235,10 @@ }, "packages/data": { "name": "@circles-sdk/data", - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "MIT", "dependencies": { - "@circles-sdk/utils": "0.5.0" + "@circles-sdk/utils": "0.5.0-preview-1" }, "devDependencies": { "typescript": "^5.3.3" @@ -4246,10 +4246,10 @@ }, "packages/profiles": { "name": "@circles-sdk/profiles", - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "ISC", "dependencies": { - "@circles-sdk/utils": "0.5.0" + "@circles-sdk/utils": "0.5.0-preview-1" }, "devDependencies": { "typescript": "^5.3.3" @@ -4257,13 +4257,13 @@ }, "packages/sdk": { "name": "@circles-sdk/sdk", - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "MIT", "dependencies": { - "@circles-sdk/abi-v1": "0.5.0", - "@circles-sdk/abi-v2": "0.5.0", - "@circles-sdk/data": "0.5.0", - "@circles-sdk/profiles": "0.5.0", + "@circles-sdk/abi-v1": "0.5.0-preview-1", + "@circles-sdk/abi-v2": "0.5.0-preview-1", + "@circles-sdk/data": "0.5.0-preview-1", + "@circles-sdk/profiles": "0.5.0-preview-1", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, @@ -4273,7 +4273,7 @@ }, "packages/utils": { "name": "@circles-sdk/utils", - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "MIT", "dependencies": { "bignumber.js": "^9.1.2" diff --git a/package.json b/package.json index f0acaa5..b4295cd 100644 --- a/package.json +++ b/package.json @@ -31,5 +31,5 @@ }, "name": "@cirlces-sdk/root", "license": "MIT", - "version": "0.5.0" + "version": "0.5.0-preview-1" } diff --git a/packages/abi-v1/package.json b/packages/abi-v1/package.json index 361df43..5cbfc43 100644 --- a/packages/abi-v1/package.json +++ b/packages/abi-v1/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/abi-v1", - "version": "0.5.0", + "version": "0.5.0-preview-1", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/abi-v2/package.json b/packages/abi-v2/package.json index 0de32c1..ca16d41 100644 --- a/packages/abi-v2/package.json +++ b/packages/abi-v2/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/abi-v2", - "version": "0.5.0", + "version": "0.5.0-preview-1", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/data/package.json b/packages/data/package.json index 9eb7f88..8a9bb52 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/data", - "version": "0.5.0", + "version": "0.5.0-preview-1", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/utils": "0.5.0" + "@circles-sdk/utils": "0.5.0-preview-1" }, "keywords": [], "author": "", diff --git a/packages/profiles/package.json b/packages/profiles/package.json index 66209b8..30a4b6e 100644 --- a/packages/profiles/package.json +++ b/packages/profiles/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/profiles", - "version": "0.5.0", + "version": "0.5.0-preview-1", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/utils": "0.5.0" + "@circles-sdk/utils": "0.5.0-preview-1" }, "keywords": [], "author": "", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 6143146..d869b25 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/sdk", - "version": "0.5.0", + "version": "0.5.0-preview-1", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,10 +17,10 @@ "author": "", "license": "MIT", "dependencies": { - "@circles-sdk/abi-v1": "0.5.0", - "@circles-sdk/abi-v2": "0.5.0", - "@circles-sdk/data": "0.5.0", - "@circles-sdk/profiles": "0.5.0", + "@circles-sdk/abi-v1": "0.5.0-preview-1", + "@circles-sdk/abi-v2": "0.5.0-preview-1", + "@circles-sdk/data": "0.5.0-preview-1", + "@circles-sdk/profiles": "0.5.0-preview-1", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, diff --git a/packages/sdk/src/circlesConfig.ts b/packages/sdk/src/circlesConfig.ts index bbaeb9b..5e0c05a 100644 --- a/packages/sdk/src/circlesConfig.ts +++ b/packages/sdk/src/circlesConfig.ts @@ -1,4 +1,5 @@ export interface CirclesConfig { + readonly v2PathfinderUrl?: string; readonly pathfinderUrl?: string; readonly circlesRpcUrl: string; readonly profileServiceUrl?: string; diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/sdk.ts index c9b4aae..c5486a6 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/sdk.ts @@ -134,9 +134,13 @@ export class Sdk implements SdkInterface { */ readonly nameRegistry?: NameRegistry; /** - * The pathfinder client. + * The pathfinder client (v1). */ readonly v1Pathfinder?: Pathfinder; + /** + * The pathfinder client (v2). + */ + readonly v2Pathfinder?: Pathfinder; /** * The profiles service client. */ @@ -160,6 +164,9 @@ export class Sdk implements SdkInterface { if (circlesConfig.pathfinderUrl) { this.v1Pathfinder = new Pathfinder(circlesConfig.pathfinderUrl); } + if (circlesConfig.v2PathfinderUrl) { + this.v2Pathfinder = new Pathfinder(circlesConfig.v2PathfinderUrl); + } if (circlesConfig.nameRegistryAddress) { this.nameRegistry = NameRegistry__factory.connect(circlesConfig.nameRegistryAddress, this.contractRunner.runner); } diff --git a/packages/sdk/src/v2/v2Avatar.ts b/packages/sdk/src/v2/v2Avatar.ts index bd224b2..669587b 100644 --- a/packages/sdk/src/v2/v2Avatar.ts +++ b/packages/sdk/src/v2/v2Avatar.ts @@ -54,9 +54,20 @@ export class V2Avatar implements AvatarInterfaceV2 { return receipt; } - getMaxTransferableAmount(to: string): Promise { - // TODO: Add v2 pathfinder - return Promise.resolve(0n); + async getMaxTransferableAmount(to: string): Promise { + this.throwIfV2IsNotAvailable(); + + const largeAmount = BigInt('999999999999999999999999999999'); + const transferPath = await this.sdk.v2Pathfinder!.getTransferPath( + this.address, + to, + largeAmount); + + if (!transferPath.isValid) { + return Promise.resolve(BigInt(0)); + } + + return transferPath.maxFlow; } async getMintableAmount(): Promise { diff --git a/packages/tests/package-lock.json b/packages/tests/package-lock.json index c06cc10..b7b8d90 100644 --- a/packages/tests/package-lock.json +++ b/packages/tests/package-lock.json @@ -27,7 +27,7 @@ }, "../abi-v1": { "name": "@circles-sdk/abi-v1", - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "MIT", "dependencies": { "ethers": "^6.11.1" @@ -38,7 +38,7 @@ }, "../abi-v2": { "name": "@circles-sdk/abi-v2", - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "MIT", "dependencies": { "ethers": "^6.11.1" @@ -85,10 +85,10 @@ }, "../data": { "name": "@circles-sdk/data", - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "MIT", "dependencies": { - "@circles-sdk/utils": "0.5.0" + "@circles-sdk/utils": "0.5.0-preview-1" }, "devDependencies": { "typescript": "^5.3.3" @@ -96,13 +96,13 @@ }, "../sdk": { "name": "@circles-sdk/sdk", - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "MIT", "dependencies": { - "@circles-sdk/abi-v1": "0.5.0", - "@circles-sdk/abi-v2": "0.5.0", - "@circles-sdk/data": "0.5.0", - "@circles-sdk/profiles": "0.5.0", + "@circles-sdk/abi-v1": "0.5.0-preview-1", + "@circles-sdk/abi-v2": "0.5.0-preview-1", + "@circles-sdk/data": "0.5.0-preview-1", + "@circles-sdk/profiles": "0.5.0-preview-1", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, @@ -111,7 +111,7 @@ } }, "../utils": { - "version": "0.5.0", + "version": "0.5.0-preview-1", "license": "MIT", "dependencies": { "bignumber.js": "^9.1.2" diff --git a/packages/utils/package.json b/packages/utils/package.json index aeaaf3a..74668ac 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/utils", - "version": "0.5.0", + "version": "0.5.0-preview-1", "description": "", "type": "module", "main": "./dist/index.js", From 0745601f16592709cb52b9c98b19589c705bed55 Mon Sep 17 00:00:00 2001 From: daniel <4954577+jaensen@users.noreply.github.com> Date: Sat, 3 Aug 2024 14:49:21 +0200 Subject: [PATCH 8/8] 0.5.0-preview-2 --- package-lock.json | 28 ++-- package.json | 2 +- packages/abi-v1/package.json | 2 +- packages/abi-v2/package.json | 2 +- packages/data/package.json | 4 +- packages/profiles/package.json | 4 +- packages/sdk/package.json | 10 +- packages/sdk/src/v2/pathfinder.ts | 253 ++++++++++++++++++++++++++++++ packages/tests/package-lock.json | 20 +-- packages/utils/package.json | 2 +- 10 files changed, 290 insertions(+), 37 deletions(-) create mode 100644 packages/sdk/src/v2/pathfinder.ts diff --git a/package-lock.json b/package-lock.json index 6515269..9512e45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cirlces-sdk/root", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cirlces-sdk/root", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "MIT", "workspaces": [ "packages/abi-v1", @@ -4213,7 +4213,7 @@ }, "packages/abi-v1": { "name": "@circles-sdk/abi-v1", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "MIT", "dependencies": { "ethers": "^6.11.1" @@ -4224,7 +4224,7 @@ }, "packages/abi-v2": { "name": "@circles-sdk/abi-v2", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "MIT", "dependencies": { "ethers": "^6.11.1" @@ -4235,10 +4235,10 @@ }, "packages/data": { "name": "@circles-sdk/data", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "MIT", "dependencies": { - "@circles-sdk/utils": "0.5.0-preview-1" + "@circles-sdk/utils": "0.5.0-preview-2" }, "devDependencies": { "typescript": "^5.3.3" @@ -4246,10 +4246,10 @@ }, "packages/profiles": { "name": "@circles-sdk/profiles", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "ISC", "dependencies": { - "@circles-sdk/utils": "0.5.0-preview-1" + "@circles-sdk/utils": "0.5.0-preview-2" }, "devDependencies": { "typescript": "^5.3.3" @@ -4257,13 +4257,13 @@ }, "packages/sdk": { "name": "@circles-sdk/sdk", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "MIT", "dependencies": { - "@circles-sdk/abi-v1": "0.5.0-preview-1", - "@circles-sdk/abi-v2": "0.5.0-preview-1", - "@circles-sdk/data": "0.5.0-preview-1", - "@circles-sdk/profiles": "0.5.0-preview-1", + "@circles-sdk/abi-v1": "0.5.0-preview-2", + "@circles-sdk/abi-v2": "0.5.0-preview-2", + "@circles-sdk/data": "0.5.0-preview-2", + "@circles-sdk/profiles": "0.5.0-preview-2", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, @@ -4273,7 +4273,7 @@ }, "packages/utils": { "name": "@circles-sdk/utils", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "MIT", "dependencies": { "bignumber.js": "^9.1.2" diff --git a/package.json b/package.json index b4295cd..9945cc9 100644 --- a/package.json +++ b/package.json @@ -31,5 +31,5 @@ }, "name": "@cirlces-sdk/root", "license": "MIT", - "version": "0.5.0-preview-1" + "version": "0.5.0-preview-2" } diff --git a/packages/abi-v1/package.json b/packages/abi-v1/package.json index 5cbfc43..abcd9a7 100644 --- a/packages/abi-v1/package.json +++ b/packages/abi-v1/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/abi-v1", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/abi-v2/package.json b/packages/abi-v2/package.json index ca16d41..73597bb 100644 --- a/packages/abi-v2/package.json +++ b/packages/abi-v2/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/abi-v2", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/data/package.json b/packages/data/package.json index 8a9bb52..a1b01ef 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/data", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/utils": "0.5.0-preview-1" + "@circles-sdk/utils": "0.5.0-preview-2" }, "keywords": [], "author": "", diff --git a/packages/profiles/package.json b/packages/profiles/package.json index 30a4b6e..94c9b9d 100644 --- a/packages/profiles/package.json +++ b/packages/profiles/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/profiles", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/utils": "0.5.0-preview-1" + "@circles-sdk/utils": "0.5.0-preview-2" }, "keywords": [], "author": "", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index d869b25..9ef8e28 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/sdk", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,10 +17,10 @@ "author": "", "license": "MIT", "dependencies": { - "@circles-sdk/abi-v1": "0.5.0-preview-1", - "@circles-sdk/abi-v2": "0.5.0-preview-1", - "@circles-sdk/data": "0.5.0-preview-1", - "@circles-sdk/profiles": "0.5.0-preview-1", + "@circles-sdk/abi-v1": "0.5.0-preview-2", + "@circles-sdk/abi-v2": "0.5.0-preview-2", + "@circles-sdk/data": "0.5.0-preview-2", + "@circles-sdk/profiles": "0.5.0-preview-2", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, diff --git a/packages/sdk/src/v2/pathfinder.ts b/packages/sdk/src/v2/pathfinder.ts new file mode 100644 index 0000000..b089594 --- /dev/null +++ b/packages/sdk/src/v2/pathfinder.ts @@ -0,0 +1,253 @@ +import {ContractTransactionReceipt} from "ethers"; +import {addressToUInt256} from "@circles-sdk/utils"; +import {Sdk} from "../sdk"; + +export interface TransferPathStep { + readonly from: string; + readonly to: string; + readonly tokenOwner: string; + readonly value: string; +} + +type ApiTransferStep = { + from: string; + to: string; + token_owner: string; + value: string; +}; + +type directPathResponse = { + data?: { + directPath?: { + requestedAmount: string; + flow: unknown; + transfers: TransferPathStep[]; + isValid?: boolean; + }; + }; +}; + +type FlowEdge = { + streamSinkId: number; + amount: bigint; +}; + +type Stream = { + sourceCoordinate: bigint, + flowEdgeIds: number[], + data: Uint8Array +} + +type FlowMatrix = { + flowVertices: string[]; + flowEdges: FlowEdge[]; + streams: Stream[]; + packedCoordinates: Uint8Array; + sourceCoordinate: number; +}; + +export class Pathfinder { + pathfinderURL: string; + + constructor(pathfinderURL: string) { + this.pathfinderURL = pathfinderURL; + } + + async getArgsForPath(from: string, to: string, value: string): Promise { + const query = { + method: 'compute_transfer', + params: {from, to, value: value.toString()} + }; + + try { + const response = await fetch(this.pathfinderURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(query) + }); + + if (!response.ok) { + throw new Error(`Error calling API: ${response.status}`); + } + + const parsed = await response.json(); + + const transformedResponse: directPathResponse = { + data: { + directPath: { + requestedAmount: value, + flow: parsed.result.maxFlowValue, + transfers: parsed.result.transferSteps.map((step: ApiTransferStep) => ({ + from: step.from, + to: step.to, + tokenOwner: step.token_owner, + value: step.value + })), + isValid: parsed.result.final + } + } + }; + + if (transformedResponse.data?.directPath) { + return createFlowMatrix(from, to, value, transformedResponse.data.directPath.transfers); + } else { + throw new Error('Invalid response from pathfinder'); + } + + } catch (error) { + if (error instanceof Error) { + throw error; + } else { + throw new Error('An unknown error occurred'); + } + } + }; +} + +// Function to create FlowMatrix from TransferPathStep[] +function createFlowMatrix(from: string, to: string, value: string, transfers: TransferPathStep[]): FlowMatrix { + const {sortedAddresses, lookUpMap} = transformToFlowVertices(transfers); + + const flowEdges: FlowEdge[] = transfers.map(transfer => ({ + streamSinkId: transfer.to === to ? 1 : 0, + amount: BigInt(transfer.value) + })); + + const flowEdgeIds: number[] = flowEdges + .map((edge, index) => (edge.streamSinkId === 1 ? index : -1)) + .filter(index => index !== -1); + + const totalTerminalAmount = flowEdges + .filter(edge => edge.streamSinkId === 1) + .reduce((sum, edge) => sum + edge.amount, BigInt(0)); + + if (totalTerminalAmount !== BigInt(value)) { + throw new Error(`The total terminal amount (${totalTerminalAmount}) does not match the provided value (${value}).`); + } + + const stream: Stream = { + sourceCoordinate: BigInt(lookUpMap[from]), + flowEdgeIds: flowEdgeIds, + data: new Uint8Array() // Empty bytes for now + }; + + const coordinates: number[] = []; + for (const transfer of transfers) { + coordinates.push(lookUpMap[transfer.tokenOwner]); + coordinates.push(lookUpMap[transfer.from]); + coordinates.push(lookUpMap[transfer.to]); + } + const packedCoordinates = packCoordinates(coordinates); + + return { + flowVertices: sortedAddresses, + flowEdges: flowEdges, + streams: [stream], + packedCoordinates: packedCoordinates, + sourceCoordinate: lookUpMap[from] + }; +} + +// Function to transform TransferPathStep[] to flow vertices array with lookup map +function transformToFlowVertices(transfers: TransferPathStep[]) { + const addressSet = new Set(); + for (const transfer of transfers) { + addressSet.add(transfer.from); + addressSet.add(transfer.to); + addressSet.add(transfer.tokenOwner); + } + + const sortedAddresses = Array.from(addressSet).sort((a, b) => { + const uint160A = BigInt(a); + const uint160B = BigInt(b); + return uint160A < uint160B ? -1 : uint160A > uint160B ? 1 : 0; + }); + + const lookUpMap: { [address: string]: number } = {}; + sortedAddresses.forEach((address, index) => { + lookUpMap[address] = index; + }); + + return { + sortedAddresses: sortedAddresses, + lookUpMap: lookUpMap + }; +} + +function packCoordinates(coordinates: number[]): Uint8Array { + const packedCoordinates = new Uint8Array(coordinates.length * 2); + for (let i = 0; i < coordinates.length; i++) { + packedCoordinates[2 * i] = coordinates[i] >> 8; // High byte + packedCoordinates[2 * i + 1] = coordinates[i] & 0xFF; // Low byte + } + return packedCoordinates; +} + +// Existing code integration +export class TransferService { + private sdk: Sdk; + private address: string; + + constructor(sdk: any, address: string) { + this.sdk = sdk; + this.address = address; + } + + private async transitiveTransfer(flowMatrix: FlowMatrix): Promise { + this.throwIfV2IsNotAvailable(); + + const {flowVertices, flowEdges, streams, packedCoordinates} = flowMatrix; + + const tx = await this.sdk.v2Hub!.operateFlowMatrix(flowVertices, flowEdges, streams, packedCoordinates); + const receipt = await tx.wait(); + if (!receipt) { + throw new Error('Transfer failed'); + } + + return receipt; + } + + private async directTransfer(to: string, amount: bigint, tokenAddress: string): Promise { + const tokenInf = await this.sdk.data.getTokenInfo(tokenAddress); + if (!tokenInf) { + throw new Error('Token not found'); + } + + const numericTokenId = addressToUInt256(tokenInf.tokenId); + const tx = await this.sdk.v2Hub?.safeTransferFrom( + this.address, + to, + numericTokenId, + amount, + new Uint8Array(0) + ); + + const receipt = await tx?.wait(); + if (!receipt) { + throw new Error('Transfer failed'); + } + + return receipt; + } + + async transfer(to: string, amount: bigint, tokenAddress?: string, pathfinder?: Pathfinder): Promise { + if (!tokenAddress) { + if (pathfinder) { + const flowMatrix = await pathfinder.getArgsForPath(this.address, to, amount.toString()); + return this.transitiveTransfer(flowMatrix); + } else { + throw new Error('Pathfinder instance required for path transfer'); + } + } else { + return this.directTransfer(to, amount, tokenAddress); + } + } + + private throwIfV2IsNotAvailable() { + if (!this.sdk.v2Hub) { + throw new Error('V2 Hub not available'); + } + } +} diff --git a/packages/tests/package-lock.json b/packages/tests/package-lock.json index b7b8d90..edfa48a 100644 --- a/packages/tests/package-lock.json +++ b/packages/tests/package-lock.json @@ -27,7 +27,7 @@ }, "../abi-v1": { "name": "@circles-sdk/abi-v1", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "MIT", "dependencies": { "ethers": "^6.11.1" @@ -38,7 +38,7 @@ }, "../abi-v2": { "name": "@circles-sdk/abi-v2", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "MIT", "dependencies": { "ethers": "^6.11.1" @@ -85,10 +85,10 @@ }, "../data": { "name": "@circles-sdk/data", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "MIT", "dependencies": { - "@circles-sdk/utils": "0.5.0-preview-1" + "@circles-sdk/utils": "0.5.0-preview-2" }, "devDependencies": { "typescript": "^5.3.3" @@ -96,13 +96,13 @@ }, "../sdk": { "name": "@circles-sdk/sdk", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "MIT", "dependencies": { - "@circles-sdk/abi-v1": "0.5.0-preview-1", - "@circles-sdk/abi-v2": "0.5.0-preview-1", - "@circles-sdk/data": "0.5.0-preview-1", - "@circles-sdk/profiles": "0.5.0-preview-1", + "@circles-sdk/abi-v1": "0.5.0-preview-2", + "@circles-sdk/abi-v2": "0.5.0-preview-2", + "@circles-sdk/data": "0.5.0-preview-2", + "@circles-sdk/profiles": "0.5.0-preview-2", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, @@ -111,7 +111,7 @@ } }, "../utils": { - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "license": "MIT", "dependencies": { "bignumber.js": "^9.1.2" diff --git a/packages/utils/package.json b/packages/utils/package.json index 74668ac..9ecdeb8 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/utils", - "version": "0.5.0-preview-1", + "version": "0.5.0-preview-2", "description": "", "type": "module", "main": "./dist/index.js",