diff --git a/packages/abi-v1/package.json b/packages/abi-v1/package.json index b655667..719ca24 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.0", + "version": "0.3.3", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/abi-v2/package.json b/packages/abi-v2/package.json index 80d52ff..d828e82 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.0", + "version": "0.3.3", "description": "", "type": "module", "main": "./dist/index.js", diff --git a/packages/data/package.json b/packages/data/package.json index 48f3c9b..1acff34 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/data", - "version": "0.3.0", + "version": "0.3.3", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "build": "rollup -c" }, "dependencies": { - "@circles-sdk/utils": "0.3.0" + "@circles-sdk/utils": "0.3.3" }, "keywords": [], "author": "", diff --git a/packages/data/src/circlesData.ts b/packages/data/src/circlesData.ts index ba3c411..92333d4 100644 --- a/packages/data/src/circlesData.ts +++ b/packages/data/src/circlesData.ts @@ -15,6 +15,7 @@ import { PagedQueryParams } from './pagedQuery/pagedQueryParams'; import { Filter } from './rpcSchema/filter'; import { GroupMembershipRow } from './rows/groupMembershipRow'; import { GroupRow } from './rows/groupRow'; +import { TokenInfoRow } from './rows/tokenInfoRow'; export class CirclesData implements CirclesDataInterface { readonly rpc: CirclesRpc; @@ -183,6 +184,10 @@ export class CirclesData implements CirclesDataInterface { }); } + /** + * Gets all trust relations of an avatar and groups mutual trust relations together. + * @param avatarAddress The address to get the trust relations for. + */ async getAggregatedTrustRelations(avatarAddress: string): Promise { const pageSize = 1000; const trustsQuery = this.getTrustRelations(avatarAddress, pageSize); @@ -253,7 +258,8 @@ export class CirclesData implements CirclesDataInterface { 'version', 'type', 'avatar', - 'tokenId' + 'tokenId', + 'cidV0Digest' ], filter: [ { @@ -293,6 +299,41 @@ export class CirclesData implements CirclesDataInterface { return returnValue; } + /** + * Gets the token info for a given token address. + * @param address The address of the token. + * @returns The token info or undefined if the token is not found. + */ + async getTokenInfo(address: string): Promise { + const circlesQuery = new CirclesQuery(this.rpc, { + namespace: 'V_Crc', + table: 'Avatars', + columns: [ + 'blockNumber', + 'timestamp', + 'transactionIndex', + 'logIndex', + 'transactionHash', + 'version', + 'type', + 'avatar', + 'tokenId' + ], + filter: [ + { + Type: 'FilterPredicate', + FilterType: 'Equals', + Column: 'tokenId', + Value: address.toLowerCase() + } + ], + sortOrder: 'ASC', + limit: 1 + }); + + return await circlesQuery.getSingleRow(); + } + /** * Subscribes to Circles events. * @param avatar The avatar to subscribe to. If not provided, all events are subscribed to. diff --git a/packages/data/src/circlesDataInterface.ts b/packages/data/src/circlesDataInterface.ts index 4c903ce..1bb3e00 100644 --- a/packages/data/src/circlesDataInterface.ts +++ b/packages/data/src/circlesDataInterface.ts @@ -9,6 +9,7 @@ import { CirclesEvent } from './events/events'; import { InvitationRow } from './rows/invitationRow'; import { GroupRow } from './rows/groupRow'; import { GroupMembershipRow } from './rows/groupMembershipRow'; +import { TokenInfoRow } from './rows/tokenInfoRow'; export interface GroupQueryParams { nameStartsWith?: string; @@ -26,6 +27,13 @@ export interface CirclesDataInterface { */ getAvatarInfo(avatar: string): Promise; + /** + * Gets the token info for a given token address. + * @param address The address of the token. + * @returns The token info or undefined if the token is not found. + */ + getTokenInfo(address: string): Promise; + /** * Gets the total CRC v1 balance of an address. * @param avatar The address to get the CRC balance for. diff --git a/packages/data/src/rows/avatarRow.ts b/packages/data/src/rows/avatarRow.ts index eb9ce11..a52192f 100644 --- a/packages/data/src/rows/avatarRow.ts +++ b/packages/data/src/rows/avatarRow.ts @@ -1,15 +1,55 @@ import { EventRow } from '../pagedQuery/eventRow'; +/** + * Contains basic information about a Circles avatar. + */ export interface AvatarRow extends EventRow { + /** + * The timestamp of the last change to the avatar. + */ timestamp: number; + /** + * The hash of the transaction that last changed the avatar. + */ transactionHash: string; + /** + * If the avatar is currently active in version 1 or 2. + * + * Note: An avatar that's active in v2 can still have a v1 token. See `hasV1` and `v1Token`. + */ version: number; - type: string; + /** + * The type of the avatar. + */ + type: 'human' | 'organization' | 'group'; + /** + * The address of the avatar. + */ avatar: string; - tokenId: string; + /** + * The personal or group token address. + * + * Note: v1 tokens are erc20 and thus have a token address. v2 tokens are erc1155 and have a tokenId. + * The v2 tokenId is always an encoded version of the avatar address. + */ + tokenId?: string; + /** + * If the avatar is signed up at v1. + */ hasV1: boolean; - v1Token: string; + /** + * If the avatar has a v1 token, this is the token address. + */ + v1Token?: string; + /** + * The CIDv0 of the avatar's metadata (profile). + */ + cidV0Digest?: string; - // Currently only set in avatar initialization + /** + * If the avatar is stopped in v1. + * + * Note: This is only set during `Avatar` initialization. + */ v1Stopped?: boolean; } \ No newline at end of file diff --git a/packages/data/src/rows/tokenInfoRow.ts b/packages/data/src/rows/tokenInfoRow.ts new file mode 100644 index 0000000..da91fa3 --- /dev/null +++ b/packages/data/src/rows/tokenInfoRow.ts @@ -0,0 +1,10 @@ +import { EventRow } from '../pagedQuery/eventRow'; + +export interface TokenInfoRow extends EventRow { + timestamp: number; + transactionHash: string; + version: number; + type: "human" | "group"; + avatar: string; + tokenId: string; +} \ No newline at end of file diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 8ab18bc..50baa7e 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/sdk", - "version": "0.3.0", + "version": "0.3.3", "description": "", "type": "module", "main": "./dist/index.js", @@ -17,9 +17,9 @@ "author": "", "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.3.3", + "@circles-sdk/abi-v2": "0.3.3", + "@circles-sdk/data": "0.3.3", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, diff --git a/packages/sdk/src/AvatarInterface.ts b/packages/sdk/src/AvatarInterface.ts index 66acd50..6707647 100644 --- a/packages/sdk/src/AvatarInterface.ts +++ b/packages/sdk/src/AvatarInterface.ts @@ -116,4 +116,10 @@ export interface AvatarInterfaceV2 extends AvatarInterface { * @param avatar The avatar's avatar. */ inviteHuman(avatar: string): Promise; + + /** + * Updates the avatar's metadata (profile). + * @param cid The IPFS CID of the metadata. + */ + updateMetadata(cid: string): Promise; } \ No newline at end of file diff --git a/packages/sdk/src/avatar.ts b/packages/sdk/src/avatar.ts index 50b1290..5ca2f2f 100644 --- a/packages/sdk/src/avatar.ts +++ b/packages/sdk/src/avatar.ts @@ -27,17 +27,28 @@ export class Avatar implements AvatarInterfaceV2 { private _avatarInfo: AvatarRow | undefined; private _sdk: Sdk; + /** + * After initialization, this property contains the avatar's basic information. + */ get avatarInfo(): AvatarRow | undefined { return this._avatarInfo; } private _tokenEventSubscription?: () => void = undefined; + /** + * Creates a new Avatar instance that controls a Circles avatar at the given address. + * @param sdk The SDK instance to use. + * @param avatarAddress The address of the avatar to control. + */ constructor(sdk: Sdk, avatarAddress: string) { this.address = avatarAddress.toLowerCase(); this._sdk = sdk; } + /** + * The events observable for this avatar. + */ public get events(): Observable { if (!this._events) { throw new Error('Not initialized'); @@ -104,18 +115,97 @@ export class Avatar implements AvatarInterfaceV2 { return func(this._avatar); } + /** + * `human` avatars can mint 24 personal Circles per day. This method returns the amount of Circles that can be minted. + * + * Note: v2 avatars can mint at max. 14 days * 24 Circles = 336 Circles. + * v1 avatars on the other hand will stop minting after 90 days without minting. + * @returns The amount of Circles that can be minted. + */ getMintableAmount = (): Promise => this.onlyIfInitialized(() => this._avatar!.getMintableAmount()); + /** + * Mints the available personal Circles for the avatar. Check `getMintableAmount()` to see how many Circles can be minted. + * @returns The transaction receipt. + */ personalMint = (): Promise => this.onlyIfInitialized(() => this._avatar!.personalMint()); + /** + * Stops the avatar's token. This will prevent any future `personalMint()` calls and is not reversible. + */ stop = (): Promise => this.onlyIfInitialized(() => this._avatar!.stop()); + /** + * Utilizes the pathfinder to find the maximum Circles amount that can be transferred from this Avatar to the other avatar. + * @param to The address to transfer the Circles to. + * @returns The maximum Circles amount that can be transferred. + */ getMaxTransferableAmount = (to: string): Promise => this.onlyIfInitialized(() => this._avatar!.getMaxTransferableAmount(to)); + /** + * Transfers Circles to another avatar. + * + * Note: The max. transferable amount can be lower than the avatar's balance depending on its trust relations and token holdings. + * Use the `getMaxTransferableAmount()` method to calculate the max. transferable amount if you need to know it beforehand. + * @param to The address of the avatar to transfer to. + * @param amount The amount to transfer. + */ transfer = (to: string, amount: bigint): Promise => this.onlyIfInitialized(() => this._avatar!.transfer(to, amount)); + /** + * Trusts another avatar. Trusting an avatar means you're willing to accept Circles that have been issued by this avatar. + * @param avatar The address of the avatar to trust. + * @returns The transaction receipt. + */ trust = (avatar: string): Promise => this.onlyIfInitialized(() => this._avatar!.trust(avatar)); + /** + * Revokes trust from another avatar. This means you will no longer accept Circles issued by this avatar. This will not affect already received Circles. + * @param avatar The address of the avatar to untrust. + * @returns The transaction receipt. + */ untrust = (avatar: string): Promise => this.onlyIfInitialized(() => this._avatar!.untrust(avatar)); + /** + * Gets the trust relations of the avatar. + * @returns An array of trust relations in this form: avatar1 - [trusts|trustedBy|mutuallyTrusts] -> avatar2. + */ getTrustRelations = (): Promise => this.onlyIfInitialized(() => this._avatar!.getTrustRelations()); + /** + * Gets the Circles transaction history of the avatar. The history contains incoming/outgoing transactions and minting of personal Circles and Group Circles. + * @param pageSize The maximum number of transactions per page. + * @returns A query object that can be used to iterate over the transaction history. + */ getTransactionHistory = (pageSize: number): Promise> => this.onlyIfInitialized(() => this._avatar!.getTransactionHistory(pageSize)); + /** + * Gets the avatar's total Circles balance. + * + * Note: This queries either the v1 or the v2 balance of an avatar. Check the `avatarInfo` property to see which version your avatar uses. + * Token holdings in v1 can be migrated to v2. Check out `Sdk.migrateAvatar` or `Sdk.migrateAllV1Tokens` for more information. + */ getTotalBalance = (): Promise => this.onlyIfInitialized(() => this._avatar!.getTotalBalance()); + /** + * Use collateral, trusted by the group, to mint new Group Circles. + * @param group The group which Circles to mint. + * @param collateral The collateral tokens to use for minting. + * @param amounts The amounts of the collateral tokens to use. + * @param data Additional data for the minting operation. + * @returns The transaction receipt. + */ groupMint = (group: string, collateral: string[], amounts: bigint[], data: Uint8Array): Promise => this.onlyIfV2((avatar) => avatar.groupMint(group, collateral, amounts, data)); + /** + * Wraps the specified amount of personal Circles into demurraged ERC20 tokens for use outside the Circles protocol. + * Note: This kind of token can be incompatible with services since it's demurraged and thus the balance changes over time. + * @param amount The amount of Circles to wrap. + */ wrapDemurrageErc20 = (amount: bigint): Promise => this.onlyIfV2((avatar) => avatar.wrapDemurrageErc20(amount)); + /** + * Wraps the specified amount of inflation Circles into ERC20 tokens for use outside the Circles protocol. + * In contrast to demurraged tokens, these token's balance does not change over time. + * @param amount + */ wrapInflationErc20 = (amount: bigint): Promise => this.onlyIfV2((avatar) => avatar.wrapInflationErc20(amount)); + /** + * Invite a human avatar to join Circles. + * @param avatar The address of any human controlled wallet. + */ inviteHuman = (avatar: string): Promise => this.onlyIfV2((_avatar) => _avatar.inviteHuman(avatar)); + /** + * Updates the avatar's metadata (profile). + * @param cid The IPFS content identifier of the metadata (Qm....). + */ + updateMetadata = (cid: string): Promise => this.onlyIfV2((_avatar) => _avatar.updateMetadata(cid)); } \ No newline at end of file diff --git a/packages/sdk/src/chainConfig.ts b/packages/sdk/src/chainConfig.ts index ecb874d..c30b270 100644 --- a/packages/sdk/src/chainConfig.ts +++ b/packages/sdk/src/chainConfig.ts @@ -3,5 +3,6 @@ export interface ChainConfig { readonly circlesRpcUrl: string; readonly v1HubAddress: string; readonly v2HubAddress?: string; + readonly nameRegistryAddress?: string; readonly migrationAddress?: string; } \ No newline at end of file diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index f248c65..33369c7 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -5,4 +5,5 @@ export { V1Person } from './v1/v1Person'; export { ChainConfig } from './chainConfig'; export { AvatarRow, TrustListRow, TrustRelationRow } from '@circles-sdk/data'; export { AvatarInterface } from './AvatarInterface'; -export { parseError } from './errors'; \ No newline at end of file +export { parseError } from './errors'; +export { Pathfinder, TransferPath, TransferStep } from './v1/pathfinder'; \ No newline at end of file diff --git a/packages/sdk/src/sdk.ts b/packages/sdk/src/sdk.ts index 164acc0..624ec53 100644 --- a/packages/sdk/src/sdk.ts +++ b/packages/sdk/src/sdk.ts @@ -7,7 +7,7 @@ import { Hub as HubV1, Hub__factory as HubV1Factory, Token__factory } from '@cir import { Hub as HubV2, Hub__factory as HubV2Factory, - Migration__factory + Migration__factory, NameRegistry, NameRegistry__factory } from '@circles-sdk/abi-v2'; import { AvatarRow, CirclesData, CirclesRpc } from '@circles-sdk/data'; import multihashes from 'multihashes'; @@ -120,13 +120,17 @@ export class Sdk implements SdkInterface { */ readonly data: CirclesData; /** - * The V1 hub contract wrapper. + * The typechain generated V1 hub contract wrapper. */ readonly v1Hub: HubV1; /** - * The V2 hub contract wrapper. + * The typechain generated V2 hub contract wrapper. */ readonly v2Hub?: HubV2; + /** + * The typechain generated NameRegistry contract wrapper. + */ + readonly nameRegistry?: NameRegistry; /** * The pathfinder client. */ @@ -150,6 +154,9 @@ export class Sdk implements SdkInterface { if (chainConfig.pathfinderUrl) { this.v1Pathfinder = new Pathfinder(chainConfig.pathfinderUrl); } + if (chainConfig.nameRegistryAddress) { + this.nameRegistry = NameRegistry__factory.connect(chainConfig.nameRegistryAddress, this.contractRunner.runner); + } } /** @@ -193,19 +200,11 @@ export class Sdk implements SdkInterface { throw new Error('V2 hub not available'); } const metadataDigest = this.cidV0Digest(cidV0); - // try { const tx = await this.v2Hub.registerHuman(metadataDigest); const receipt = await tx.wait(); if (!receipt) { throw new Error('Transaction failed'); } - // } catch (e) { - // const revertData = (e).message.replace('Reverted ', ''); - // parseError(revertData); - // console.log('Caught error:'); - // console.error(e); - // throw e; - // } const signerAddress = this.contractRunner.address; await this.waitForAvatarInfo(signerAddress); diff --git a/packages/sdk/src/v2/v2Person.ts b/packages/sdk/src/v2/v2Person.ts index 352c10c..00e8926 100644 --- a/packages/sdk/src/v2/v2Person.ts +++ b/packages/sdk/src/v2/v2Person.ts @@ -26,8 +26,6 @@ export class V2Person implements AvatarInterfaceV2 { return this.avatarInfo.avatar; } - // TODO: Empty stream makes no sense - // readonly events: Observable = Observable.create().property; public readonly avatarInfo: AvatarRow; constructor(sdk: Sdk, avatarInfo: AvatarRow) { @@ -39,6 +37,19 @@ export class V2Person implements AvatarInterfaceV2 { } } + async updateMetadata(cid: string): Promise { + this.throwIfNameRegistryIsNotAvailable(); + + const digest = this.sdk.cidV0Digest(cid); + const tx = await this.sdk.nameRegistry?.updateMetadataDigest(digest); + const receipt = await tx?.wait(); + if (!receipt) { + throw new Error('Transfer failed'); + } + + return receipt; + } + getMaxTransferableAmount(to: string): Promise { // TODO: Add v2 pathfinder return Promise.resolve(0n); @@ -227,4 +238,10 @@ export class V2Person implements AvatarInterfaceV2 { throw new Error('V2 is not available'); } } + + private throwIfNameRegistryIsNotAvailable() { + if (!this.sdk.nameRegistry) { + throw new Error('Name registry is not available'); + } + } } \ No newline at end of file diff --git a/packages/tests/package-lock.json b/packages/tests/package-lock.json index c0553ef..0052096 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.0", + "version": "0.3.3", "license": "ISC", "dependencies": { "ethers": "^6.11.1" @@ -39,7 +39,7 @@ }, "../abi-v2": { "name": "@circles-sdk/abi-v2", - "version": "0.3.0", + "version": "0.3.3", "license": "ISC", "dependencies": { "ethers": "^6.11.1" @@ -84,10 +84,10 @@ }, "../data": { "name": "@circles-sdk/data", - "version": "0.3.0", + "version": "0.3.3", "license": "ISC", "dependencies": { - "@circles-sdk/utils": "0.3.0" + "@circles-sdk/utils": "0.3.3" }, "devDependencies": { "typescript": "^5.3.3" @@ -95,12 +95,12 @@ }, "../sdk": { "name": "@circles-sdk/sdk", - "version": "0.3.0", + "version": "0.3.3", "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.3.3", + "@circles-sdk/abi-v2": "0.3.3", + "@circles-sdk/data": "0.3.3", "ethers": "^6.11.1", "multihashes": "^4.0.3" }, diff --git a/packages/utils/package.json b/packages/utils/package.json index 8af459f..dee28d8 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@circles-sdk/utils", - "version": "0.3.0", + "version": "0.3.3", "description": "", "type": "module", "main": "./dist/index.js",