From 16bed76ec302a84d6a465ba394ab9d55aeba8342 Mon Sep 17 00:00:00 2001 From: Javier R Date: Sat, 19 Oct 2024 17:46:14 +0200 Subject: [PATCH 1/4] add: drips `sdk` package scaffold --- packages/sdk/package.json | 35 + .../common/drivers/address-driver-abi.ts | 436 ++++++++++ .../drips/common/drivers/address-driver.ts | 46 ++ .../drips/common/drivers/nft-driver-abi.ts | 766 ++++++++++++++++++ .../src/drips/common/drivers/nft-driver.ts | 109 +++ .../populate-create-new-stream-flow-txs.ts | 65 ++ .../sdk/src/drips/common/graphql/dripQL.ts | 37 + .../drips/common/managers/MetadataManager.ts | 204 +++++ .../managers/NftDriverMetadataManager.ts | 92 +++ .../drips/common/schemas/address-driver/v1.ts | 61 ++ .../drips/common/schemas/common/sources.ts | 11 + .../sdk/src/drips/common/schemas/index.ts | 28 + .../src/drips/common/schemas/nft-driver/v1.ts | 26 + .../src/drips/common/schemas/nft-driver/v2.ts | 41 + .../src/drips/common/schemas/nft-driver/v3.ts | 7 + .../src/drips/common/schemas/nft-driver/v4.ts | 7 + .../drips/common/schemas/repo-driver/v1.ts | 33 + .../drips/common/schemas/repo-driver/v2.ts | 35 + .../drips/common/schemas/repo-driver/v3.ts | 28 + .../drips/common/schemas/repo-driver/v4.ts | 18 + .../src/drips/common/sdk/address-driver.ts | 66 ++ .../sdk/src/drips/common/sdk/caller-abi.ts | 284 +++++++ packages/sdk/src/drips/common/sdk/caller.ts | 34 + packages/sdk/src/drips/common/sdk/erc20.ts | 72 ++ .../drips/common/services/DripListService.ts | 393 +++++++++ .../sdk/src/drips/common/services/GitHub.ts | 68 ++ packages/sdk/src/drips/common/store/mock.ts | 38 + packages/sdk/src/drips/common/types.ts | 132 +++ .../drips/common/utils/contract-constants.ts | 15 + .../utils/extract-address-from-account.ts | 36 + .../drips/common/utils/extract-driver-id.ts | 32 + .../common/utils/filter-current-chain-data.ts | 48 ++ .../common/utils/format-split-receivers.ts | 33 + .../common/utils/format-stream-receivers.ts | 33 + .../drips/common/utils/get-own-account-id.ts | 16 + packages/sdk/src/drips/common/utils/ipfs.ts | 20 + .../common/utils/is-address-driver-id.ts | 18 + .../sdk/src/drips/common/utils/is-claimed.ts | 23 + .../common/utils/key-value-to-metadata.ts | 15 + .../sdk/src/drips/common/utils/make-stream.ts | 61 ++ .../drips/common/utils/random-big-until.ts | 18 + .../src/drips/common/utils/settlement-date.ts | 34 + .../src/drips/common/utils/stream-config.ts | 85 ++ .../sdk/src/drips/common/utils/streams.ts | 294 +++++++ .../drips/common/utils/tx-to-caller-call.ts | 10 + .../drips/common/utils/tx-to-safe-drips.ts | 12 + .../drips/common/utils/unwrap-ether-result.ts | 11 + .../drips/common/wallet/FailOverProvider.ts | 108 +++ packages/sdk/src/drips/common/wallet/mock.ts | 65 ++ .../sdk/src/drips/common/wallet/network.ts | 325 ++++++++ .../sdk/src/drips/common/wallet/provider.ts | 60 ++ packages/sdk/src/drips/createDripList.ts | 9 + packages/sdk/src/index.ts | 1 + packages/sdk/tsconfig.json | 21 + 54 files changed, 4575 insertions(+) create mode 100644 packages/sdk/package.json create mode 100644 packages/sdk/src/drips/common/drivers/address-driver-abi.ts create mode 100644 packages/sdk/src/drips/common/drivers/address-driver.ts create mode 100644 packages/sdk/src/drips/common/drivers/nft-driver-abi.ts create mode 100644 packages/sdk/src/drips/common/drivers/nft-driver.ts create mode 100644 packages/sdk/src/drips/common/drivers/populate-create-new-stream-flow-txs.ts create mode 100644 packages/sdk/src/drips/common/graphql/dripQL.ts create mode 100644 packages/sdk/src/drips/common/managers/MetadataManager.ts create mode 100644 packages/sdk/src/drips/common/managers/NftDriverMetadataManager.ts create mode 100644 packages/sdk/src/drips/common/schemas/address-driver/v1.ts create mode 100644 packages/sdk/src/drips/common/schemas/common/sources.ts create mode 100644 packages/sdk/src/drips/common/schemas/index.ts create mode 100644 packages/sdk/src/drips/common/schemas/nft-driver/v1.ts create mode 100644 packages/sdk/src/drips/common/schemas/nft-driver/v2.ts create mode 100644 packages/sdk/src/drips/common/schemas/nft-driver/v3.ts create mode 100644 packages/sdk/src/drips/common/schemas/nft-driver/v4.ts create mode 100644 packages/sdk/src/drips/common/schemas/repo-driver/v1.ts create mode 100644 packages/sdk/src/drips/common/schemas/repo-driver/v2.ts create mode 100644 packages/sdk/src/drips/common/schemas/repo-driver/v3.ts create mode 100644 packages/sdk/src/drips/common/schemas/repo-driver/v4.ts create mode 100644 packages/sdk/src/drips/common/sdk/address-driver.ts create mode 100644 packages/sdk/src/drips/common/sdk/caller-abi.ts create mode 100644 packages/sdk/src/drips/common/sdk/caller.ts create mode 100644 packages/sdk/src/drips/common/sdk/erc20.ts create mode 100644 packages/sdk/src/drips/common/services/DripListService.ts create mode 100644 packages/sdk/src/drips/common/services/GitHub.ts create mode 100644 packages/sdk/src/drips/common/store/mock.ts create mode 100644 packages/sdk/src/drips/common/types.ts create mode 100644 packages/sdk/src/drips/common/utils/contract-constants.ts create mode 100644 packages/sdk/src/drips/common/utils/extract-address-from-account.ts create mode 100644 packages/sdk/src/drips/common/utils/extract-driver-id.ts create mode 100644 packages/sdk/src/drips/common/utils/filter-current-chain-data.ts create mode 100644 packages/sdk/src/drips/common/utils/format-split-receivers.ts create mode 100644 packages/sdk/src/drips/common/utils/format-stream-receivers.ts create mode 100644 packages/sdk/src/drips/common/utils/get-own-account-id.ts create mode 100644 packages/sdk/src/drips/common/utils/ipfs.ts create mode 100644 packages/sdk/src/drips/common/utils/is-address-driver-id.ts create mode 100644 packages/sdk/src/drips/common/utils/is-claimed.ts create mode 100644 packages/sdk/src/drips/common/utils/key-value-to-metadata.ts create mode 100644 packages/sdk/src/drips/common/utils/make-stream.ts create mode 100644 packages/sdk/src/drips/common/utils/random-big-until.ts create mode 100644 packages/sdk/src/drips/common/utils/settlement-date.ts create mode 100644 packages/sdk/src/drips/common/utils/stream-config.ts create mode 100644 packages/sdk/src/drips/common/utils/streams.ts create mode 100644 packages/sdk/src/drips/common/utils/tx-to-caller-call.ts create mode 100644 packages/sdk/src/drips/common/utils/tx-to-safe-drips.ts create mode 100644 packages/sdk/src/drips/common/utils/unwrap-ether-result.ts create mode 100644 packages/sdk/src/drips/common/wallet/FailOverProvider.ts create mode 100644 packages/sdk/src/drips/common/wallet/mock.ts create mode 100644 packages/sdk/src/drips/common/wallet/network.ts create mode 100644 packages/sdk/src/drips/common/wallet/provider.ts create mode 100644 packages/sdk/src/drips/createDripList.ts create mode 100644 packages/sdk/src/index.ts create mode 100644 packages/sdk/tsconfig.json diff --git a/packages/sdk/package.json b/packages/sdk/package.json new file mode 100644 index 0000000..f383e7c --- /dev/null +++ b/packages/sdk/package.json @@ -0,0 +1,35 @@ +{ + "name": "@kariba/sdk", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "format": "prettier --write ." + }, + "keywords": [], + "authors": [ + { + "name": "Javier Ríos", + "email": "javi@karibalabs.co" + } + ], + "license": "ISC", + "devDependencies": { + "@types/node": "^22.7.6", + "prettier": "^3.3.3", + "typescript": "~5.6.3" + }, + "dependencies": { + "@apollo/client": "^3.11.8", + "@efstajas/versioned-parser": "^0.1.4", + "@octokit/rest": "^21.0.2", + "abitype": "^1.0.6", + "add": "^2.0.6", + "ethers": "^6.13.4", + "graphql": "^16.9.0", + "graphql-request": "^7.1.0", + "lodash": "^4.17.21", + "zod": "^3.23.8" + } +} diff --git a/packages/sdk/src/drips/common/drivers/address-driver-abi.ts b/packages/sdk/src/drips/common/drivers/address-driver-abi.ts new file mode 100644 index 0000000..c8c3447 --- /dev/null +++ b/packages/sdk/src/drips/common/drivers/address-driver-abi.ts @@ -0,0 +1,436 @@ +export const addressDriverAbi = [ + { + inputs: [ + { internalType: "contract Drips", name: "drips_", type: "address" }, + { internalType: "address", name: "forwarder", type: "address" }, + { internalType: "uint32", name: "driverId_", type: "uint32" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "previousAdmin", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "AdminChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beacon", + type: "address", + }, + ], + name: "BeaconUpgraded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "currentAdmin", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "NewAdminProposed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "pauser", + type: "address", + }, + ], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "pauser", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "admin", + type: "address", + }, + ], + name: "PauserGranted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "pauser", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "admin", + type: "address", + }, + ], + name: "PauserRevoked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "pauser", + type: "address", + }, + ], + name: "Unpaused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "implementation", + type: "address", + }, + ], + name: "Upgraded", + type: "event", + }, + { + inputs: [], + name: "acceptAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "admin", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "allPausers", + outputs: [ + { + internalType: "address[]", + name: "pausersList", + type: "address[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "addr", type: "address" }], + name: "calcAccountId", + outputs: [ + { + internalType: "uint256", + name: "accountId", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "contract IERC20", name: "erc20", type: "address" }, + { internalType: "address", name: "transferTo", type: "address" }, + ], + name: "collect", + outputs: [{ internalType: "uint128", name: "amt", type: "uint128" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "drips", + outputs: [ + { + internalType: "contract Drips", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "driverId", + outputs: [{ internalType: "uint32", name: "", type: "uint32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "bytes", name: "value", type: "bytes" }, + ], + internalType: "struct AccountMetadata[]", + name: "accountMetadata", + type: "tuple[]", + }, + ], + name: "emitAccountMetadata", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "receiver", type: "uint256" }, + { internalType: "contract IERC20", name: "erc20", type: "address" }, + { internalType: "uint128", name: "amt", type: "uint128" }, + ], + name: "give", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "pauser", type: "address" }], + name: "grantPauser", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "implementation", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "isPaused", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "pauser", type: "address" }], + name: "isPauser", + outputs: [{ internalType: "bool", name: "isAddrPauser", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "forwarder", + type: "address", + }, + ], + name: "isTrustedForwarder", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "proposeNewAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "proposedAdmin", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "proxiableUUID", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "renounceAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "pauser", type: "address" }], + name: "revokePauser", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "uint256", + name: "accountId", + type: "uint256", + }, + { internalType: "uint32", name: "weight", type: "uint32" }, + ], + internalType: "struct SplitsReceiver[]", + name: "receivers", + type: "tuple[]", + }, + ], + name: "setSplits", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "contract IERC20", name: "erc20", type: "address" }, + { + components: [ + { + internalType: "uint256", + name: "accountId", + type: "uint256", + }, + { + internalType: "StreamConfig", + name: "config", + type: "uint256", + }, + ], + internalType: "struct StreamReceiver[]", + name: "currReceivers", + type: "tuple[]", + }, + { internalType: "int128", name: "balanceDelta", type: "int128" }, + { + components: [ + { + internalType: "uint256", + name: "accountId", + type: "uint256", + }, + { + internalType: "StreamConfig", + name: "config", + type: "uint256", + }, + ], + internalType: "struct StreamReceiver[]", + name: "newReceivers", + type: "tuple[]", + }, + { internalType: "uint32", name: "maxEndHint1", type: "uint32" }, + { internalType: "uint32", name: "maxEndHint2", type: "uint32" }, + { internalType: "address", name: "transferTo", type: "address" }, + ], + name: "setStreams", + outputs: [ + { + internalType: "int128", + name: "realBalanceDelta", + type: "int128", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "unpause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newImplementation", + type: "address", + }, + ], + name: "upgradeTo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newImplementation", + type: "address", + }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "upgradeToAndCall", + outputs: [], + stateMutability: "payable", + type: "function", + }, +] as const; + +export type AddressDriverAbi = typeof addressDriverAbi; diff --git a/packages/sdk/src/drips/common/drivers/address-driver.ts b/packages/sdk/src/drips/common/drivers/address-driver.ts new file mode 100644 index 0000000..cf25732 --- /dev/null +++ b/packages/sdk/src/drips/common/drivers/address-driver.ts @@ -0,0 +1,46 @@ +import type { + AbiFunction, + AbiParametersToPrimitiveTypes, + ExtractAbiFunction, + ExtractAbiFunctionNames, +} from "abitype"; +import { Contract } from "ethers"; +import { UnwrappedEthersResult } from "&/drips/common/types.js"; +import network from "&/drips/common/wallet/network.js"; +import { + AddressDriverAbi, + addressDriverAbi, +} from "&/drips/common/drivers/address-driver-abi.js"; +import { get } from "&/drips/common/store/mock.js"; +import unwrapEthersResult from "&/drips/common/utils/unwrap-ether-result.js"; + +export async function executeAddressDriverReadMethod< + functionName extends ExtractAbiFunctionNames< + AddressDriverAbi, + "pure" | "view" + >, + abiFunction extends AbiFunction = ExtractAbiFunction< + AddressDriverAbi, + functionName + >, +>(config: { + functionName: + | functionName + | ExtractAbiFunctionNames; + args: AbiParametersToPrimitiveTypes; +}): Promise< + UnwrappedEthersResult< + AbiParametersToPrimitiveTypes + > +> { + const { provider } = await get(); + const { functionName: func, args } = config; + + const addressDriver = new Contract( + network.contracts.ADDRESS_DRIVER, + addressDriverAbi, + provider, + ); + + return unwrapEthersResult(await addressDriver[func](...args)); +} diff --git a/packages/sdk/src/drips/common/drivers/nft-driver-abi.ts b/packages/sdk/src/drips/common/drivers/nft-driver-abi.ts new file mode 100644 index 0000000..875eab6 --- /dev/null +++ b/packages/sdk/src/drips/common/drivers/nft-driver-abi.ts @@ -0,0 +1,766 @@ +export const nftDriverAbi = [ + { + inputs: [ + { internalType: "contract Drips", name: "drips_", type: "address" }, + { internalType: "address", name: "forwarder", type: "address" }, + { internalType: "uint32", name: "driverId_", type: "uint32" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "previousAdmin", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "AdminChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "approved", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { + indexed: false, + internalType: "bool", + name: "approved", + type: "bool", + }, + ], + name: "ApprovalForAll", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beacon", + type: "address", + }, + ], + name: "BeaconUpgraded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "currentAdmin", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "NewAdminProposed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "pauser", + type: "address", + }, + ], + name: "Paused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "pauser", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "admin", + type: "address", + }, + ], + name: "PauserGranted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "pauser", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "admin", + type: "address", + }, + ], + name: "PauserRevoked", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "to", + type: "address", + }, + { + indexed: true, + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + name: "Transfer", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "pauser", + type: "address", + }, + ], + name: "Unpaused", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "implementation", + type: "address", + }, + ], + name: "Upgraded", + type: "event", + }, + { + inputs: [], + name: "acceptAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "admin", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "allPausers", + outputs: [ + { + internalType: "address[]", + name: "pausersList", + type: "address[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "approve", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "owner", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "burn", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "minter", type: "address" }, + { internalType: "uint64", name: "salt", type: "uint64" }, + ], + name: "calcTokenIdWithSalt", + outputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "tokenId", type: "uint256" }, + { internalType: "contract IERC20", name: "erc20", type: "address" }, + { internalType: "address", name: "transferTo", type: "address" }, + ], + name: "collect", + outputs: [{ internalType: "uint128", name: "amt", type: "uint128" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "drips", + outputs: [ + { + internalType: "contract Drips", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "driverId", + outputs: [{ internalType: "uint32", name: "", type: "uint32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "tokenId", type: "uint256" }, + { + components: [ + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "bytes", name: "value", type: "bytes" }, + ], + internalType: "struct AccountMetadata[]", + name: "accountMetadata", + type: "tuple[]", + }, + ], + name: "emitAccountMetadata", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "getApproved", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "tokenId", type: "uint256" }, + { internalType: "uint256", name: "receiver", type: "uint256" }, + { internalType: "contract IERC20", name: "erc20", type: "address" }, + { internalType: "uint128", name: "amt", type: "uint128" }, + ], + name: "give", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "pauser", type: "address" }], + name: "grantPauser", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "implementation", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "operator", type: "address" }, + ], + name: "isApprovedForAll", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "isPaused", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "pauser", type: "address" }], + name: "isPauser", + outputs: [{ internalType: "bool", name: "isAddrPauser", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "minter", type: "address" }, + { internalType: "uint64", name: "salt", type: "uint64" }, + ], + name: "isSaltUsed", + outputs: [{ internalType: "bool", name: "isUsed", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "forwarder", + type: "address", + }, + ], + name: "isTrustedForwarder", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { + components: [ + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "bytes", name: "value", type: "bytes" }, + ], + internalType: "struct AccountMetadata[]", + name: "accountMetadata", + type: "tuple[]", + }, + ], + name: "mint", + outputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint64", name: "salt", type: "uint64" }, + { internalType: "address", name: "to", type: "address" }, + { + components: [ + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "bytes", name: "value", type: "bytes" }, + ], + internalType: "struct AccountMetadata[]", + name: "accountMetadata", + type: "tuple[]", + }, + ], + name: "mintWithSalt", + outputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "pure", + type: "function", + }, + { + inputs: [], + name: "nextTokenId", + outputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "ownerOf", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "proposeNewAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "proposedAdmin", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "proxiableUUID", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "renounceAdmin", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "pauser", type: "address" }], + name: "revokePauser", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { + components: [ + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "bytes", name: "value", type: "bytes" }, + ], + internalType: "struct AccountMetadata[]", + name: "accountMetadata", + type: "tuple[]", + }, + ], + name: "safeMint", + outputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint64", name: "salt", type: "uint64" }, + { internalType: "address", name: "to", type: "address" }, + { + components: [ + { internalType: "bytes32", name: "key", type: "bytes32" }, + { internalType: "bytes", name: "value", type: "bytes" }, + ], + internalType: "struct AccountMetadata[]", + name: "accountMetadata", + type: "tuple[]", + }, + ], + name: "safeMintWithSalt", + outputs: [ + { + internalType: "uint256", + name: "tokenId", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "safeTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bool", name: "approved", type: "bool" }, + ], + name: "setApprovalForAll", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "tokenId", type: "uint256" }, + { + components: [ + { + internalType: "uint256", + name: "accountId", + type: "uint256", + }, + { internalType: "uint32", name: "weight", type: "uint32" }, + ], + internalType: "struct SplitsReceiver[]", + name: "receivers", + type: "tuple[]", + }, + ], + name: "setSplits", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "tokenId", type: "uint256" }, + { internalType: "contract IERC20", name: "erc20", type: "address" }, + { + components: [ + { + internalType: "uint256", + name: "accountId", + type: "uint256", + }, + { + internalType: "StreamConfig", + name: "config", + type: "uint256", + }, + ], + internalType: "struct StreamReceiver[]", + name: "currReceivers", + type: "tuple[]", + }, + { internalType: "int128", name: "balanceDelta", type: "int128" }, + { + components: [ + { + internalType: "uint256", + name: "accountId", + type: "uint256", + }, + { + internalType: "StreamConfig", + name: "config", + type: "uint256", + }, + ], + internalType: "struct StreamReceiver[]", + name: "newReceivers", + type: "tuple[]", + }, + { internalType: "uint32", name: "maxEndHint1", type: "uint32" }, + { internalType: "uint32", name: "maxEndHint2", type: "uint32" }, + { internalType: "address", name: "transferTo", type: "address" }, + ], + name: "setStreams", + outputs: [ + { + internalType: "int128", + name: "realBalanceDelta", + type: "int128", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "interfaceId", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "pure", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "tokenURI", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "tokenId", type: "uint256" }, + ], + name: "transferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "unpause", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newImplementation", + type: "address", + }, + ], + name: "upgradeTo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newImplementation", + type: "address", + }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "upgradeToAndCall", + outputs: [], + stateMutability: "payable", + type: "function", + }, +] as const; + +export type NftDriverAbi = typeof nftDriverAbi; diff --git a/packages/sdk/src/drips/common/drivers/nft-driver.ts b/packages/sdk/src/drips/common/drivers/nft-driver.ts new file mode 100644 index 0000000..e7bbebd --- /dev/null +++ b/packages/sdk/src/drips/common/drivers/nft-driver.ts @@ -0,0 +1,109 @@ +import type { + AbiFunction, + AbiParametersToPrimitiveTypes, + ExtractAbiFunction, + ExtractAbiFunctionNames, +} from "abitype"; +import { + type NftDriverAbi, + nftDriverAbi, +} from "&/drips/common/drivers/nft-driver-abi.js"; +import type { UnwrappedEthersResult } from "&/drips/common/types.js"; +import { get } from "&/drips/common/store/mock.js"; +import { Contract, ContractTransaction, TransactionResponse } from "ethers"; +import network from "&/drips/common/wallet/network.js"; +import unwrapEthersResult from "&/drips/common/utils/unwrap-ether-result.js"; +import assert from "node:assert"; +import txToSafeDripsTx from "&/drips/common/utils/tx-to-safe-drips.js"; + +export async function executeNftDriverReadMethod< + functionName extends ExtractAbiFunctionNames, + abiFunction extends AbiFunction = ExtractAbiFunction< + NftDriverAbi, + functionName + >, +>(config: { + functionName: + | functionName + | ExtractAbiFunctionNames; + args: AbiParametersToPrimitiveTypes; +}): Promise< + UnwrappedEthersResult< + AbiParametersToPrimitiveTypes + > +> { + const { provider } = await get(); + const { functionName: func, args } = config; + + const nftDriver = new Contract( + network.contracts.NFT_DRIVER, + nftDriverAbi, + provider, + ); + + return unwrapEthersResult(await nftDriver[func](...args)); +} + +export async function executeNftDriverWriteMethod< + functionName extends ExtractAbiFunctionNames< + NftDriverAbi, + "nonpayable" | "payable" + >, + abiFunction extends AbiFunction = ExtractAbiFunction< + NftDriverAbi, + functionName + >, +>(config: { + functionName: + | functionName + | ExtractAbiFunctionNames; + args: AbiParametersToPrimitiveTypes; +}): Promise { + const { signer } = await get(); + assert( + signer, + "NFT Driver contract call requires a signer but it is missing.", + ); + + const { functionName: func, args } = config; + + const nftDriver = new Contract( + network.contracts.NFT_DRIVER, + nftDriverAbi, + signer, + ); + + return nftDriver[func](...args); +} + +export async function populateNftDriverWriteTx< + functionName extends ExtractAbiFunctionNames< + NftDriverAbi, + "nonpayable" | "payable" + >, + abiFunction extends AbiFunction = ExtractAbiFunction< + NftDriverAbi, + functionName + >, +>(config: { + functionName: + | functionName + | ExtractAbiFunctionNames; + args: AbiParametersToPrimitiveTypes; +}): Promise { + const { signer } = await get(); + assert( + signer, + "NFT Driver contract call requires a signer but it is missing.", + ); + + const { functionName: func, args } = config; + + const nftDriver = new Contract( + network.contracts.NFT_DRIVER, + nftDriverAbi, + signer, + ); + + return txToSafeDripsTx(await nftDriver[func].populateTransaction(...args)); +} diff --git a/packages/sdk/src/drips/common/drivers/populate-create-new-stream-flow-txs.ts b/packages/sdk/src/drips/common/drivers/populate-create-new-stream-flow-txs.ts new file mode 100644 index 0000000..c129148 --- /dev/null +++ b/packages/sdk/src/drips/common/drivers/populate-create-new-stream-flow-txs.ts @@ -0,0 +1,65 @@ +import { ContractTransaction } from "ethers"; +import { populateAddressDriverWriteTx } from "../sdk/address-driver.js"; +import { MetadataKeyValue, OxString, StreamConfig } from "../types.js"; +import { streamConfigToUint256 } from "../utils/stream-config.js"; +import contractConstants from "../utils/contract-constants.js"; +import { formatStreamReceivers } from "../utils/format-stream-receivers.js"; + +type NewStreamFlowPayload = { + tokenAddress: OxString; + currentReceivers: { accountId: bigint; config: StreamConfig }[]; + newReceivers: { accountId: bigint; config: StreamConfig }[]; + balanceDelta: bigint; + transferToAddress: OxString; + accountMetadata: MetadataKeyValue[]; +}; + +export default async function populateNewStreamFlowTxs( + payload: NewStreamFlowPayload, +): Promise { + const { + accountMetadata, + tokenAddress, + newReceivers, + balanceDelta, + currentReceivers, + transferToAddress, + } = payload; + if ( + currentReceivers.length > contractConstants.MAX_DRIPS_RECEIVERS || + newReceivers.length > contractConstants.MAX_DRIPS_RECEIVERS + ) { + throw new Error( + `Too many receivers. Max allowed is ${contractConstants.MAX_DRIPS_RECEIVERS}.`, + ); + } + + const setStreamsTx = await populateAddressDriverWriteTx({ + functionName: "setStreams", + args: [ + tokenAddress, + formatStreamReceivers( + currentReceivers.map((receiver) => ({ + accountId: receiver.accountId, + config: streamConfigToUint256(receiver.config), + })), + ), + balanceDelta, + formatStreamReceivers( + newReceivers.map((receiver) => ({ + accountId: receiver.accountId, + config: streamConfigToUint256(receiver.config), + })), + ), + 0, + 0, + transferToAddress, + ], + }); + + const emitAccountMetadataTx = await populateAddressDriverWriteTx({ + functionName: "emitAccountMetadata", + args: [accountMetadata], + }); + return [setStreamsTx, emitAccountMetadataTx]; +} diff --git a/packages/sdk/src/drips/common/graphql/dripQL.ts b/packages/sdk/src/drips/common/graphql/dripQL.ts new file mode 100644 index 0000000..5b97c98 --- /dev/null +++ b/packages/sdk/src/drips/common/graphql/dripQL.ts @@ -0,0 +1,37 @@ +import uniqBy from "lodash/uniqBy"; +import { parse } from "graphql"; +import { + GraphQLClient, + type RequestDocument, + type Variables, +} from "graphql-request"; +import { addTypenameToDocument } from "@apollo/client/utilities"; +import { getOptionalEnvVar } from "&/drips/common/wallet/provider.js"; + +const envBaseUrl = getOptionalEnvVar("PUBLIC_BASE_URL"); +const BASE_URL = envBaseUrl ?? "https://localhost:5173"; + +export default async function query< + TResponse, + TVariables extends Variables = Variables, +>( + query: RequestDocument, + variables?: TVariables, + customFetch: typeof fetch = fetch, +): Promise { + const client = new GraphQLClient(`${BASE_URL}/api/gql`, { + fetch: customFetch, + }); + + const parsedQuery = typeof query === "string" ? parse(query) : query; + + const queryWithTypenames = addTypenameToDocument(parsedQuery); + + return await client.request( + { + ...queryWithTypenames, + definitions: uniqBy(queryWithTypenames.definitions, "name.value"), + }, + variables, + ); +} diff --git a/packages/sdk/src/drips/common/managers/MetadataManager.ts b/packages/sdk/src/drips/common/managers/MetadataManager.ts new file mode 100644 index 0000000..5e1fe12 --- /dev/null +++ b/packages/sdk/src/drips/common/managers/MetadataManager.ts @@ -0,0 +1,204 @@ +import { z } from "zod"; +import type { + AnyVersion, + LatestVersion, + Parser, +} from "@efstajas/versioned-parser"; +import type { + AccountId, + IpfsHash, + MetadataKeyValue, +} from "&/drips/common/types.js"; +import { + type ContractTransaction, + toBigInt, + type TransactionResponse, +} from "ethers"; +import { executeNftDriverWriteMethod } from "&/drips/common/drivers/nft-driver.js"; +import { fetchIpfs as ipfsFetch } from "&/drips/common/utils/ipfs.js"; +import assert from "node:assert"; +import keyValueToMetatada from "&/drips/common/utils/key-value-to-metadata.js"; + +export interface IMetadataManager { + fetchMetadataHashByAccountId(accountId: AccountId): Promise; + + fetchAccountMetadata( + accountId: AccountId, + ): Promise<{ hash: IpfsHash; data: AnyVersion } | null>; + + pinAccountMetadata(data: LatestVersion): Promise; + + updateAccountMetadata( + newData: z.infer, + lastKnownHash: IpfsHash | undefined, + schema: T, + ): Promise<{ newHash: IpfsHash; tx: TransactionResponse }>; + + buildAccountMetadata(context: unknown): LatestVersion; +} + +export type EmitMetadataFunc = ( + accountId: string, + accountMetadata: MetadataKeyValue[], +) => Promise; + +export default abstract class MetadataManagerBase + implements IMetadataManager +{ + public static readonly USER_METADATA_KEY = "ipfs"; + + private readonly _parser: TParser; + private readonly _emitMetadataFunc: + | typeof executeNftDriverWriteMethod + | undefined; + + protected constructor( + parser: TParser, + emitMetadataFunc?: + | typeof executeNftDriverWriteMethod + | typeof executeNftDriverWriteMethod, + ) { + this._parser = parser; + this._emitMetadataFunc = emitMetadataFunc; + } + + /** + * Builds account metadata. + * @param context The context to build the account metadata from. + * @returns The built account metadata. + */ + public abstract buildAccountMetadata( + context: unknown, + ): LatestVersion; + + /** + * Upgrades metadata in a format matching any version to the latest version. This is used to + * ensure that metadata is in the latest format when updating an account that had previously + * written metadata in an older format. + * @param currentMetadata The current metadata to upgrade. + * @returns The upgraded metadata. + */ + public abstract upgradeAccountMetadata( + currentMetadata: AnyVersion, + ): LatestVersion; + + /** + * Fetches the latest metadata hash for a given user ID. + * @param accountId The user ID to fetch the metadata hash for. + * @returns The latest metadata hash for the given user ID, or null if no metadata hash exists. + */ + public abstract fetchMetadataHashByAccountId( + accountId: AccountId, + ): Promise; + + private async fetchIpfs(hash: IpfsHash) { + return await (await ipfsFetch(hash)).json(); + } + + /** + * Fetches the latest IPFS metadata for a given user ID. + * @param accountId The user ID to fetch the metadata for. + * @returns The latest IPFS metadata for the given user ID, or null if no metadata exists. + */ + public async fetchAccountMetadata( + accountId: AccountId, + ): Promise<{ hash: IpfsHash; data: AnyVersion } | null> { + const metadataHash = await this.fetchMetadataHashByAccountId(accountId); + if (!metadataHash) return null; + + let accountMetadataRes: Awaited< + ReturnType + >; + + try { + accountMetadataRes = await this.fetchIpfs(metadataHash); + } catch { + return null; + } + + return { + hash: metadataHash, + data: this._parser.parseAny(accountMetadataRes) as AnyVersion, + }; + } + + /** + * Pins account metadata to IPFS. + * @param data The account metadata to pin. + * @returns The IPFS hash of the pinned metadata. + * @throws If the pinning fails. + */ + public async pinAccountMetadata( + data: LatestVersion, + ): Promise { + // Ensure the data follows the correct schema at runtime + this._parser.parseLatest(data); + + const res = await fetch("/api/ipfs/pin", { + method: "POST", + body: JSON.stringify(data, (_, value) => + typeof value === "bigint" ? value.toString() : value, + ), + }); + + if (!res.ok) { + throw new Error(`Pinning account metadata failed: ${await res.text()}`); + } + + return res.text(); + } + + /** + * Updates account metadata. + * @param newData The new account metadata. + * @param lastKnownHash The last known IPFS hash of the account metadata. + * @returns The new IPFS hash of the account metadata, and the transaction that emitted the new metadata. + * @throws If the last known hash doesnʼt match the on-chain value. + * @throws If the update fails. + */ + public async updateAccountMetadata( + newData: z.infer, + lastKnownHash: IpfsHash | undefined, + ): Promise<{ newHash: IpfsHash; tx: TransactionResponse }> { + const { accountId } = newData.describes; + const currentOnChainHash = + await this.fetchMetadataHashByAccountId(accountId); + + if (currentOnChainHash !== lastKnownHash) { + throw new Error( + "Current metadata hash doesnʼt match on-chain value." + + "If your account was edited elsewhere previously, please refresh the page before making further changes.", + ); + } + + const newHash = await this.pinAccountMetadata(newData); + + const tx = await this.emitAccountMetadata(newHash, accountId); + + return { + newHash, + tx, + }; + } + + private async emitAccountMetadata(newHash: IpfsHash, accountId: AccountId) { + assert( + this._emitMetadataFunc, + "emitAccountMetadata called without emitMetadataFunc", + ); + + const accountMetadata = [ + keyValueToMetatada({ + key: MetadataManagerBase.USER_METADATA_KEY, + value: newHash, + }), + ]; + + const tx = await this._emitMetadataFunc({ + functionName: "emitAccountMetadata", + args: [toBigInt(accountId), accountMetadata], + }); + + return tx; + } +} diff --git a/packages/sdk/src/drips/common/managers/NftDriverMetadataManager.ts b/packages/sdk/src/drips/common/managers/NftDriverMetadataManager.ts new file mode 100644 index 0000000..464fde9 --- /dev/null +++ b/packages/sdk/src/drips/common/managers/NftDriverMetadataManager.ts @@ -0,0 +1,92 @@ +import { AnyVersion, LatestVersion } from "@efstajas/versioned-parser"; +import { gql } from "graphql-request"; +import { executeNftDriverWriteMethod } from "&/drips/common/drivers/nft-driver.js"; +import network from "&/drips/common/wallet/network.js"; +import MetadataManagerBase from "&/drips/common/managers/MetadataManager.js"; +import { nftDriverAccountMetadataParser } from "&/drips/common/schemas/index.js"; +import query from "&/drips/common/graphql/dripQL.js"; + +export default class NftDriverMetadataManager extends MetadataManagerBase< + typeof nftDriverAccountMetadataParser +> { + constructor(nftDriver?: typeof executeNftDriverWriteMethod) { + super(nftDriverAccountMetadataParser, nftDriver); + } + + public async fetchMetadataHashByAccountId( + accountId: string, + ): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const res = await query( + gql` + query LatestDripListMetadataHash( + $accountId: ID! + $chain: SupportedChain! + ) { + dripList(id: $accountId, chain: $chain) { + latestMetadataIpfsHash + } + } + `, + { accountId, chain: network.gqlName }, + ); + + return res.dripList?.latestMetadataIpfsHash ?? null; + } + + public buildAccountMetadata(context: { + forAccountId: string; + projects: LatestVersion["projects"]; + name?: string; + description?: string; + latestVotingRoundId?: string; + }): LatestVersion { + const { forAccountId, projects, name, description, latestVotingRoundId } = + context; + + return { + driver: "nft", + describes: { + driver: "nft", + accountId: forAccountId, + }, + isDripList: true, + projects, + name, + description, + latestVotingRoundId, + }; + } + + public upgradeAccountMetadata( + currentMetadata: AnyVersion, + ): LatestVersion { + const result = currentMetadata; + + type Projects = AnyVersion< + typeof nftDriverAccountMetadataParser + >["projects"][number]; + + const upgradeSplit = (split: Projects) => { + if ("type" in split) return split; + + if ("source" in split) { + return { + type: "repoDriver" as const, + ...split, + }; + } else { + return { + type: "address" as const, + ...split, + }; + } + }; + + result.projects = result.projects.map(upgradeSplit); + + const parsed = nftDriverAccountMetadataParser.parseLatest(result); + + return parsed; + } +} diff --git a/packages/sdk/src/drips/common/schemas/address-driver/v1.ts b/packages/sdk/src/drips/common/schemas/address-driver/v1.ts new file mode 100644 index 0000000..f7428e9 --- /dev/null +++ b/packages/sdk/src/drips/common/schemas/address-driver/v1.ts @@ -0,0 +1,61 @@ +import { isAddress } from "ethers"; +import { z } from "zod"; + +const ethAddressSchema = z.preprocess((v) => { + if (typeof v !== "string" || !isAddress(v)) { + throw new Error(`${v} is not a valid address`); + } + + return v; +}, z.string()); + +const bigintSchema = z.preprocess( + (v) => (typeof v === "string" ? BigInt(v) : v), + z.bigint(), +); + +const streamConfigSchema = z.object({ + raw: z.string(), + dripId: z.string(), + amountPerSecond: bigintSchema, + /** If zero, the stream runs indefinitely. */ + durationSeconds: z.number(), + /** + * If undefined, the block timestamp from the initial setStreams event + * corresponding to this stream should be considered as the stream + * start date. + */ + startTimestamp: z.number().optional(), +}); + +const dripsUserSchema = z.object({ + driver: z.union([z.literal("address"), z.literal("nft"), z.literal("repo")]), + accountId: z.string(), +}); + +const streamMetadataSchema = z.object({ + id: z.string(), + initialDripsConfig: streamConfigSchema, + receiver: dripsUserSchema, + archived: z.boolean(), + name: z.string().optional(), + description: z.string().optional(), +}); + +const assetConfigMetadataSchema = z.object({ + tokenAddress: ethAddressSchema, + streams: z.array(streamMetadataSchema), +}); + +export const addressDriverAccountMetadataSchemaV1 = z.object({ + describes: z.object({ + driver: z.literal("address"), + accountId: z.string(), + }), + name: z.string().optional(), + description: z.string().optional(), + emoji: z.string().optional(), + assetConfigs: z.array(assetConfigMetadataSchema), + timestamp: z.number(), + writtenByAddress: ethAddressSchema, +}); diff --git a/packages/sdk/src/drips/common/schemas/common/sources.ts b/packages/sdk/src/drips/common/schemas/common/sources.ts new file mode 100644 index 0000000..a714c96 --- /dev/null +++ b/packages/sdk/src/drips/common/schemas/common/sources.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +const gitHubSourceSchema = z.object({ + forge: z.literal("github"), + repoName: z.string(), + ownerName: z.string(), + url: z.string(), +}); + +// This will be a union type when we add support for other forges. +export const sourceSchema = gitHubSourceSchema; diff --git a/packages/sdk/src/drips/common/schemas/index.ts b/packages/sdk/src/drips/common/schemas/index.ts new file mode 100644 index 0000000..4911775 --- /dev/null +++ b/packages/sdk/src/drips/common/schemas/index.ts @@ -0,0 +1,28 @@ +import { createVersionedParser } from "@efstajas/versioned-parser"; +import { addressDriverAccountMetadataSchemaV1 } from "&/drips/common/schemas/address-driver/v1.js"; +import { nftDriverAccountMetadataSchemaV1 } from "&/drips/common/schemas/nft-driver/v1.js"; +import { nftDriverAccountMetadataSchemaV2 } from "&/drips/common/schemas/nft-driver/v2.js"; +import { nftDriverAccountMetadataSchemaV3 } from "&/drips/common/schemas/nft-driver/v3.js"; +import { repoDriverAccountMetadataSchemaV1 } from "&/drips/common/schemas/repo-driver/v1.js"; +import { repoDriverAccountMetadataSchemaV2 } from "&/drips/common/schemas/repo-driver/v2.js"; +import { repoDriverAccountMetadataSchemaV3 } from "&/drips/common/schemas/repo-driver/v3.js"; +import { repoDriverAccountMetadataSchemaV4 } from "&/drips/common/schemas/repo-driver/v4.js"; +import { nftDriverAccountMetadataSchemaV4 } from "&/drips/common/schemas/nft-driver/v4.js"; + +export const nftDriverAccountMetadataParser = createVersionedParser([ + nftDriverAccountMetadataSchemaV4.parse, + nftDriverAccountMetadataSchemaV3.parse, + nftDriverAccountMetadataSchemaV2.parse, + nftDriverAccountMetadataSchemaV1.parse, +]); + +export const addressDriverAccountMetadataParser = createVersionedParser([ + addressDriverAccountMetadataSchemaV1.parse, +]); + +export const repoDriverAccountMetadataParser = createVersionedParser([ + repoDriverAccountMetadataSchemaV4.parse, + repoDriverAccountMetadataSchemaV3.parse, + repoDriverAccountMetadataSchemaV2.parse, + repoDriverAccountMetadataSchemaV1.parse, +]); diff --git a/packages/sdk/src/drips/common/schemas/nft-driver/v1.ts b/packages/sdk/src/drips/common/schemas/nft-driver/v1.ts new file mode 100644 index 0000000..0df819a --- /dev/null +++ b/packages/sdk/src/drips/common/schemas/nft-driver/v1.ts @@ -0,0 +1,26 @@ +import { z } from "zod"; +import { sourceSchema } from "&/drips/common/schemas/common/sources.js"; + +const addressDriverSplitReceiverSchema = z.object({ + weight: z.number(), + accountId: z.string(), +}); + +const repoDriverSplitReceiverSchema = z.object({ + weight: z.number(), + accountId: z.string(), + source: sourceSchema, +}); + +export const nftDriverAccountMetadataSchemaV1 = z.object({ + driver: z.literal("nft"), + describes: z.object({ + driver: z.literal("nft"), + accountId: z.string(), + }), + isDripList: z.literal(true), + projects: z.array( + z.union([repoDriverSplitReceiverSchema, addressDriverSplitReceiverSchema]), + ), + name: z.string().optional(), +}); diff --git a/packages/sdk/src/drips/common/schemas/nft-driver/v2.ts b/packages/sdk/src/drips/common/schemas/nft-driver/v2.ts new file mode 100644 index 0000000..a19e63f --- /dev/null +++ b/packages/sdk/src/drips/common/schemas/nft-driver/v2.ts @@ -0,0 +1,41 @@ +import { z } from "zod"; +import { sourceSchema } from "&/drips/common/schemas/common/sources.js"; + +export const addressDriverSplitReceiverSchema = z.object({ + type: z.literal("address"), + weight: z.number(), + accountId: z.string(), +}); + +export const repoDriverSplitReceiverSchema = z.object({ + type: z.literal("repoDriver"), + weight: z.number(), + accountId: z.string(), + source: sourceSchema, +}); + +/** + * A splits entry that splits directly to a different Drip List. + */ +export const dripListSplitReceiverSchema = z.object({ + type: z.literal("dripList"), + weight: z.number(), + accountId: z.string(), +}); + +export const nftDriverAccountMetadataSchemaV2 = z.object({ + driver: z.literal("nft"), + describes: z.object({ + driver: z.literal("nft"), + accountId: z.string(), + }), + isDripList: z.literal(true), + projects: z.array( + z.union([ + dripListSplitReceiverSchema, + repoDriverSplitReceiverSchema, + addressDriverSplitReceiverSchema, + ]), + ), + name: z.string().optional(), +}); diff --git a/packages/sdk/src/drips/common/schemas/nft-driver/v3.ts b/packages/sdk/src/drips/common/schemas/nft-driver/v3.ts new file mode 100644 index 0000000..faac052 --- /dev/null +++ b/packages/sdk/src/drips/common/schemas/nft-driver/v3.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; +import { nftDriverAccountMetadataSchemaV2 } from "&/drips/common/schemas/nft-driver/v2.js"; + +export const nftDriverAccountMetadataSchemaV3 = + nftDriverAccountMetadataSchemaV2.extend({ + description: z.string().optional(), + }); diff --git a/packages/sdk/src/drips/common/schemas/nft-driver/v4.ts b/packages/sdk/src/drips/common/schemas/nft-driver/v4.ts new file mode 100644 index 0000000..e7c1faf --- /dev/null +++ b/packages/sdk/src/drips/common/schemas/nft-driver/v4.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; +import { nftDriverAccountMetadataSchemaV3 } from "&/drips/common/schemas/nft-driver/v3.js"; + +export const nftDriverAccountMetadataSchemaV4 = + nftDriverAccountMetadataSchemaV3.extend({ + latestVotingRoundId: z.string().optional(), + }); diff --git a/packages/sdk/src/drips/common/schemas/repo-driver/v1.ts b/packages/sdk/src/drips/common/schemas/repo-driver/v1.ts new file mode 100644 index 0000000..88e5002 --- /dev/null +++ b/packages/sdk/src/drips/common/schemas/repo-driver/v1.ts @@ -0,0 +1,33 @@ +import { z } from "zod"; +import { sourceSchema } from "&/drips/common/schemas/common/sources.js"; + +const addressDriverSplitReceiverSchema = z.object({ + weight: z.number(), + accountId: z.string(), +}); + +const repoDriverSplitReceiverSchema = z.object({ + weight: z.number(), + accountId: z.string(), + source: sourceSchema, +}); + +const repoDriverAccountSplitsSchema = z.object({ + maintainers: z.array(addressDriverSplitReceiverSchema), + dependencies: z.array( + z.union([repoDriverSplitReceiverSchema, addressDriverSplitReceiverSchema]), + ), +}); + +export const repoDriverAccountMetadataSchemaV1 = z.object({ + driver: z.literal("repo"), + describes: z.object({ + driver: z.literal("repo"), + accountId: z.string(), + }), + source: sourceSchema, + emoji: z.string(), + color: z.string(), + description: z.string().optional(), + splits: repoDriverAccountSplitsSchema, +}); diff --git a/packages/sdk/src/drips/common/schemas/repo-driver/v2.ts b/packages/sdk/src/drips/common/schemas/repo-driver/v2.ts new file mode 100644 index 0000000..4607f59 --- /dev/null +++ b/packages/sdk/src/drips/common/schemas/repo-driver/v2.ts @@ -0,0 +1,35 @@ +import { z } from "zod"; +import { sourceSchema } from "&/drips/common/schemas/common/sources.js"; + +export const addressDriverSplitReceiverSchema = z.object({ + type: z.literal("address"), + weight: z.number(), + accountId: z.string(), +}); + +export const repoDriverSplitReceiverSchema = z.object({ + type: z.literal("repoDriver"), + weight: z.number(), + accountId: z.string(), + source: sourceSchema, +}); + +const repoDriverAccountSplitsSchema = z.object({ + maintainers: z.array(addressDriverSplitReceiverSchema), + dependencies: z.array( + z.union([repoDriverSplitReceiverSchema, addressDriverSplitReceiverSchema]), + ), +}); + +export const repoDriverAccountMetadataSchemaV2 = z.object({ + driver: z.literal("repo"), + describes: z.object({ + driver: z.literal("repo"), + accountId: z.string(), + }), + source: sourceSchema, + emoji: z.string(), + color: z.string(), + description: z.string().optional(), + splits: repoDriverAccountSplitsSchema, +}); diff --git a/packages/sdk/src/drips/common/schemas/repo-driver/v3.ts b/packages/sdk/src/drips/common/schemas/repo-driver/v3.ts new file mode 100644 index 0000000..339b793 --- /dev/null +++ b/packages/sdk/src/drips/common/schemas/repo-driver/v3.ts @@ -0,0 +1,28 @@ +import { z } from "zod"; +import { + addressDriverSplitReceiverSchema, + repoDriverAccountMetadataSchemaV2, + repoDriverSplitReceiverSchema, +} from "&/drips/common/schemas/repo-driver/v2.js"; + +const dripListSplitReceiverSchema = z.object({ + type: z.literal("dripList"), + weight: z.number(), + accountId: z.string(), +}); + +const repoDriverAccountSplitsSchema = z.object({ + maintainers: z.array(addressDriverSplitReceiverSchema), + dependencies: z.array( + z.union([ + dripListSplitReceiverSchema, + repoDriverSplitReceiverSchema, + addressDriverSplitReceiverSchema, + ]), + ), +}); + +export const repoDriverAccountMetadataSchemaV3 = + repoDriverAccountMetadataSchemaV2.extend({ + splits: repoDriverAccountSplitsSchema, + }); diff --git a/packages/sdk/src/drips/common/schemas/repo-driver/v4.ts b/packages/sdk/src/drips/common/schemas/repo-driver/v4.ts new file mode 100644 index 0000000..682529f --- /dev/null +++ b/packages/sdk/src/drips/common/schemas/repo-driver/v4.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; +import { repoDriverAccountMetadataSchemaV3 } from "&/drips/common/schemas/repo-driver/v3.js"; + +const emojiAvatarSchema = z.object({ + type: z.literal("emoji"), + emoji: z.string(), +}); + +const imageAvatarSchema = z.object({ + type: z.literal("image"), + cid: z.string(), +}); + +export const repoDriverAccountMetadataSchemaV4 = + repoDriverAccountMetadataSchemaV3.extend({ + emoji: z.undefined().optional(), + avatar: z.union([emojiAvatarSchema, imageAvatarSchema]), + }); diff --git a/packages/sdk/src/drips/common/sdk/address-driver.ts b/packages/sdk/src/drips/common/sdk/address-driver.ts new file mode 100644 index 0000000..fdf8abf --- /dev/null +++ b/packages/sdk/src/drips/common/sdk/address-driver.ts @@ -0,0 +1,66 @@ +import { executeErc20ReadMethod } from "&/drips/common/sdk/erc20.js"; +import assert from "assert"; +import { get } from "&/drips/common/store/mock.js"; +import { OxString } from "&/drips/common/types.js"; +import network from "&/drips/common/wallet/network.js"; +import { + AbiFunction, + AbiParametersToPrimitiveTypes, + ExtractAbiFunction, + ExtractAbiFunctionNames, +} from "abitype"; +import { Contract, ContractTransaction } from "ethers"; +import { + AddressDriverAbi, + addressDriverAbi, +} from "../drivers/address-driver-abi.js"; +import txToSafeDripsTx from "../utils/tx-to-safe-drips.js"; + +export async function populateAddressDriverWriteTx< + functionName extends ExtractAbiFunctionNames< + AddressDriverAbi, + "nonpayable" | "payable" + >, + abiFunction extends AbiFunction = ExtractAbiFunction< + AddressDriverAbi, + functionName + >, +>(config: { + functionName: + | functionName + | ExtractAbiFunctionNames; + args: AbiParametersToPrimitiveTypes; +}): Promise { + const { signer } = await get(); + assert( + signer, + "Address driver contract call requires a signer but it is missing.", + ); + + const { functionName: func, args } = config; + + const addressDriver = new Contract( + network.contracts.ADDRESS_DRIVER, + addressDriverAbi, + signer, + ); + + return txToSafeDripsTx( + await addressDriver[func].populateTransaction(...args), + ); +} + +export async function getAddressDriverAllowance( + token: OxString, +): Promise { + const owner = (await get()).signer?.address as OxString; + assert(owner, "ERC20 contract call requires a signer but it is missing."); + + const spender = network.contracts.ADDRESS_DRIVER as OxString; + + return executeErc20ReadMethod({ + token, + functionName: "allowance", + args: [owner, spender], + }); +} diff --git a/packages/sdk/src/drips/common/sdk/caller-abi.ts b/packages/sdk/src/drips/common/sdk/caller-abi.ts new file mode 100644 index 0000000..97713c2 --- /dev/null +++ b/packages/sdk/src/drips/common/sdk/caller-abi.ts @@ -0,0 +1,284 @@ +export const callerAbi = [ + { inputs: [], name: "InvalidShortString", type: "error" }, + { + inputs: [{ internalType: "string", name: "str", type: "string" }], + name: "StringTooLong", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "authorized", + type: "address", + }, + ], + name: "Authorized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "authorized", + type: "address", + }, + ], + name: "CalledAs", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "nonce", + type: "uint256", + }, + ], + name: "CalledSigned", + type: "event", + }, + { + anonymous: false, + inputs: [], + name: "EIP712DomainChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "newNonce", + type: "uint256", + }, + ], + name: "NonceSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "unauthorized", + type: "address", + }, + ], + name: "Unauthorized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "UnauthorizedAll", + type: "event", + }, + { + inputs: [], + name: "MAX_NONCE_INCREASE", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "sender", type: "address" }], + name: "allAuthorized", + outputs: [ + { + internalType: "address[]", + name: "authorized", + type: "address[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "authorize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "address", name: "target", type: "address" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "callAs", + outputs: [{ internalType: "bytes", name: "returnData", type: "bytes" }], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "address", + name: "target", + type: "address", + }, + { internalType: "bytes", name: "data", type: "bytes" }, + { internalType: "uint256", name: "value", type: "uint256" }, + ], + internalType: "struct Call[]", + name: "calls", + type: "tuple[]", + }, + ], + name: "callBatched", + outputs: [ + { + internalType: "bytes[]", + name: "returnData", + type: "bytes[]", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "address", name: "target", type: "address" }, + { internalType: "bytes", name: "data", type: "bytes" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { internalType: "bytes32", name: "r", type: "bytes32" }, + { internalType: "bytes32", name: "sv", type: "bytes32" }, + ], + name: "callSigned", + outputs: [{ internalType: "bytes", name: "returnData", type: "bytes" }], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "eip712Domain", + outputs: [ + { internalType: "bytes1", name: "fields", type: "bytes1" }, + { internalType: "string", name: "name", type: "string" }, + { internalType: "string", name: "version", type: "string" }, + { internalType: "uint256", name: "chainId", type: "uint256" }, + { + internalType: "address", + name: "verifyingContract", + type: "address", + }, + { internalType: "bytes32", name: "salt", type: "bytes32" }, + { + internalType: "uint256[]", + name: "extensions", + type: "uint256[]", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "address", name: "user", type: "address" }, + ], + name: "isAuthorized", + outputs: [{ internalType: "bool", name: "authorized", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "forwarder", + type: "address", + }, + ], + name: "isTrustedForwarder", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "sender", type: "address" }], + name: "nonce", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "newNonce", + type: "uint256", + }, + ], + name: "setNonce", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "unauthorize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "unauthorizeAll", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +export type CallerAbi = typeof callerAbi; diff --git a/packages/sdk/src/drips/common/sdk/caller.ts b/packages/sdk/src/drips/common/sdk/caller.ts new file mode 100644 index 0000000..c47fdc0 --- /dev/null +++ b/packages/sdk/src/drips/common/sdk/caller.ts @@ -0,0 +1,34 @@ +import { type CallerAbi, callerAbi } from "&/drips/common/sdk/caller-abi.js"; +import { + AbiFunction, + AbiParametersToPrimitiveTypes, + ExtractAbiFunction, + ExtractAbiFunctionNames, +} from "abitype"; +import assert from "assert"; +import { Contract, ContractTransaction } from "ethers"; +import { get } from "&/drips/common/store/mock.js"; +import txToSafeDripsTx from "../utils/tx-to-safe-drips.js"; +import network from "../wallet/network.js"; + +export async function populateCallerWriteTx< + functionName extends ExtractAbiFunctionNames< + CallerAbi, + "nonpayable" | "payable" + >, + abiFunction extends AbiFunction = ExtractAbiFunction, +>(config: { + functionName: + | functionName + | ExtractAbiFunctionNames; + args: AbiParametersToPrimitiveTypes; +}): Promise { + const { signer } = await get(); + assert(signer, "Caller contract call requires a signer but it is missing."); + + const { functionName: func, args } = config; + + const caller = new Contract(network.contracts.CALLER, callerAbi, signer); + + return txToSafeDripsTx(await caller[func].populateTransaction(...args)); +} diff --git a/packages/sdk/src/drips/common/sdk/erc20.ts b/packages/sdk/src/drips/common/sdk/erc20.ts new file mode 100644 index 0000000..5105167 --- /dev/null +++ b/packages/sdk/src/drips/common/sdk/erc20.ts @@ -0,0 +1,72 @@ +import { + AbiFunction, + AbiParametersToPrimitiveTypes, + ExtractAbiFunction, + ExtractAbiFunctionNames, +} from "abitype"; +import { erc20Abi } from "abitype/abis"; +import assert from "assert"; +import { Contract, ContractTransaction } from "ethers"; +import { get } from "&/drips/common/store/mock.js"; +import { OxString, UnwrappedEthersResult } from "&/drips/common/types.js"; +import txToSafeDripsTx from "&/drips/common/utils/tx-to-safe-drips.js"; +import unwrapEthersResult from "&/drips/common/utils/unwrap-ether-result.js"; +import network from "&/drips/common/wallet/network.js"; + +type Erc20Abi = typeof erc20Abi; + +export async function executeErc20ReadMethod< + functionName extends ExtractAbiFunctionNames, + abiFunction extends AbiFunction = ExtractAbiFunction, +>(config: { + token: OxString; + functionName: + | functionName + | ExtractAbiFunctionNames; + args: AbiParametersToPrimitiveTypes; +}): Promise< + UnwrappedEthersResult< + AbiParametersToPrimitiveTypes + > +> { + const { provider } = await get(); + const { token, functionName: func, args } = config; + + const erc20 = new Contract(token, erc20Abi, provider); + + return unwrapEthersResult(await erc20[func](...args)); +} + +export async function getAddressDriverAllowance(token: OxString) { + const signer = (await get()).signer?.address; + assert(signer, "ERC20 contract call requires a signer but it is missing."); + + return executeErc20ReadMethod({ + token, + functionName: "allowance", + args: [signer as OxString, network.contracts.ADDRESS_DRIVER as OxString], + }); +} + +export async function populateErc20WriteTx< + functionName extends ExtractAbiFunctionNames< + Erc20Abi, + "nonpayable" | "payable" + >, + abiFunction extends AbiFunction = ExtractAbiFunction, +>(config: { + token: OxString; + functionName: + | functionName + | ExtractAbiFunctionNames; + args: AbiParametersToPrimitiveTypes; +}): Promise { + const { signer } = await get(); + assert(signer, "ERC20 contract call requires a signer but it is missing."); + + const { token, functionName: func, args } = config; + + const erc20 = new Contract(token, erc20Abi, signer); + + return txToSafeDripsTx(await erc20[func].populateTransaction(...args)); +} diff --git a/packages/sdk/src/drips/common/services/DripListService.ts b/packages/sdk/src/drips/common/services/DripListService.ts new file mode 100644 index 0000000..85ab8f7 --- /dev/null +++ b/packages/sdk/src/drips/common/services/DripListService.ts @@ -0,0 +1,393 @@ +import { + ContractTransaction, + ethers, + MaxUint256, + type Signer, + toBigInt, +} from "ethers"; +import type { + AccountId, + Address, + IpfsHash, + Items, + OxString, + SplitsReceiver, + Weights, +} from "&/drips/common/types.js"; +import NftDriverMetadataManager from "&/drips/common/managers/NftDriverMetadataManager.js"; +import { get } from "&/drips/common/store/mock.js"; +import assert from "node:assert"; +import { + executeNftDriverReadMethod, + executeNftDriverWriteMethod, + populateNftDriverWriteTx, +} from "&/drips/common/drivers/nft-driver.js"; +import { LatestVersion } from "@efstajas/versioned-parser"; +import { gql } from "graphql-request"; +import query from "&/drips/common/graphql/dripQL.js"; +import MetadataManagerBase from "&/drips/common/managers/MetadataManager.js"; +import { nftDriverAccountMetadataParser } from "&/drips/common/schemas/index.js"; +import keyValueToMetatada from "&/drips/common/utils/key-value-to-metadata.js"; +import network from "&/drips/common/wallet/network.js"; +import { formatSplitReceivers } from "&/drips/common/utils/format-split-receivers.js"; +import { + getAddressDriverAllowance, + populateAddressDriverWriteTx, +} from "&/drips/common/sdk/address-driver.js"; +import { populateCallerWriteTx } from "../sdk/caller.js"; +import { populateErc20WriteTx } from "../sdk/erc20.js"; +import txToCallerCall from "../utils/tx-to-caller-call.js"; +import { buildStreamCreateBatchTx } from "../utils/streams.js"; + +export class KDripListService { + private readonly SEED_CONSTANT = "Drips App"; + private _owner!: Signer | undefined; + private _ownerAddress!: Address | undefined; + private _nftDriverMetadataManager!: NftDriverMetadataManager; + + private constructor() {} + + public static async new(): Promise { + const dripListService = new KDripListService(); + + const { connected, signer } = await get(); + + if (connected) { + assert(signer, "Signer address is undefined."); + dripListService._owner = signer; + dripListService._ownerAddress = await signer.getAddress(); + + dripListService._nftDriverMetadataManager = new NftDriverMetadataManager( + executeNftDriverWriteMethod, + ); + } else { + dripListService._nftDriverMetadataManager = + new NftDriverMetadataManager(); + } + + return dripListService; + } + + public async buildTransactContext(config: { + listTitle: string; + listDescription?: string; + weights: Weights; + items: Items; + support?: + | { + type: "continuous"; + tokenAddress: string; + amountPerSec: bigint; + topUpAmount: bigint; + } + | { + type: "one-time"; + tokenAddress: string; + donationAmount: bigint; + }; + latestVotingRoundId?: string; + }) { + assert( + this._ownerAddress, + `This function requires an active wallet connection.`, + ); + + const { + listTitle, + listDescription, + weights, + items, + support, + latestVotingRoundId, + } = config; + + const { projectsSplitMetadata, receivers } = + await this.getProjectsSplitMetadataAndReceivers(weights, items); + + const mintedNftAccountsCountQuery = gql` + query MintedNftAccountsCount( + $ownerAddress: String! + $chain: SupportedChain! + ) { + mintedTokensCountByOwnerAddress( + ownerAddress: $ownerAddress + chain: $chain + ) { + total + } + } + `; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mintedNftAccountsCountRes = await query( + mintedNftAccountsCountQuery, + { + ownerAddress: this._ownerAddress, + chain: network.gqlName, + }, + ); + + const salt = this._calcSaltFromAddress( + this._ownerAddress, + mintedNftAccountsCountRes.mintedTokensCountByOwnerAddress.total ?? 0, + ); + + const listId = ( + await executeNftDriverReadMethod({ + functionName: "calcTokenIdWithSalt", + args: [this._ownerAddress as OxString, salt], + }) + ).toString(); + + const ipfsHash = await this._publishMetadataToIpfs( + listId, + projectsSplitMetadata, + listTitle, + listDescription, + latestVotingRoundId, + ); + + const createDripListTx = await this._buildCreateDripListTx(salt, ipfsHash); + + const setDripListSplitsTx = await populateNftDriverWriteTx({ + functionName: "setSplits", + args: [toBigInt(listId), formatSplitReceivers(receivers)], + }); + + let needsApprovalForToken: string | undefined; + let txs: ContractTransaction[]; + + if (support?.type !== "continuous") { + throw new Error("Only continuous support is supported."); + } + + const { tokenAddress, amountPerSec, topUpAmount } = support; + + const allowance = await getAddressDriverAllowance(tokenAddress as OxString); + const needsApproval = allowance < topUpAmount; + + if (needsApproval) { + needsApprovalForToken = tokenAddress; + } + + const setStreamTx = await this._buildSetDripListStreamTxs( + tokenAddress, + listId, + topUpAmount, + amountPerSec, + ); + + txs = [createDripListTx, setDripListSplitsTx, ...setStreamTx.batch]; + + const batch = await populateCallerWriteTx({ + functionName: "callBatched", + args: [txs.map(txToCallerCall)], + }); + + return { + txs: [ + ...(needsApprovalForToken + ? [ + { + title: `Approve Drips to withdraw ${needsApprovalForToken}`, + transaction: await this._buildTokenApprovalTx( + needsApprovalForToken, + ), + waitingSignatureMessage: { + message: `Waiting for you to approve Drips access to the ERC-20 token in your wallet...`, + subtitle: "You only have to do this once per token.", + }, + applyGasBuffer: false, + }, + ] + : []), + { + title: "Creating the Drip List", + transaction: batch, + applyGasBuffer: true, + }, + ], + dripListId: listId, + }; + } + + public async getProjectsSplitMetadataAndReceivers( + weights: Weights, + items: Items, + ) { + const projectsInput = Object.entries(weights); + + const receivers: SplitsReceiver[] = []; + + const projectsSplitMetadata: ReturnType< + typeof nftDriverAccountMetadataParser.parseLatest + >["projects"] = []; + + for (const [accountId, weight] of projectsInput) { + const item = items[accountId]; + if (weight <= 0) continue; + + switch (item.type) { + case "address": { + const receiver = { + type: "address" as const, + weight, + accountId, + }; + + projectsSplitMetadata.push(receiver); + receivers.push(receiver); + + break; + } + case "project": { + const { forge, ownerName, repoName } = item.project.source; + + const receiver = { + type: "repoDriver" as const, + weight, + accountId, + }; + + // projectsSplitMetadata.push({ + // ...receiver, + // // TODO: implement GitProjectService + // source: GitProjectService.populateSource( + // forge, + // repoName, + // ownerName, + // ), + // }); + + receivers.push(receiver); + + break; + } + case "drip-list": { + const receiver = { + type: "dripList" as const, + weight, + accountId, + }; + + projectsSplitMetadata.push(receiver); + receivers.push(receiver); + + break; + } + } + } + + return { + projectsSplitMetadata, + receivers: receivers, + }; + } + + private async _buildCreateDripListTx(salt: bigint, ipfsHash: IpfsHash) { + assert( + this._ownerAddress, + `This function requires an active wallet connection.`, + ); + + const createDripListTx = await populateNftDriverWriteTx({ + functionName: "safeMintWithSalt", + args: [ + salt, + this._ownerAddress as OxString, + [ + { + key: MetadataManagerBase.USER_METADATA_KEY, + value: ipfsHash, + }, + ].map(keyValueToMetatada), + ], + }); + + return createDripListTx; + } + + private async _buildSetDripListStreamTxs( + token: Address, + dripListId: AccountId, + topUpAmount: bigint, + amountPerSec: bigint, + ) { + assert(this._owner, `This function requires an active wallet connection.`); + + return await buildStreamCreateBatchTx( + this._owner, + { + tokenAddress: token, + amountPerSecond: amountPerSec, + recipientAccountId: dripListId, + name: undefined, + }, + topUpAmount, + ); + } + + private async _buildTokenApprovalTx( + token: Address, + ): Promise { + assert(this._owner, `This function requires an active wallet connection.`); + + const tokenApprovalTx = await populateErc20WriteTx({ + token: token as OxString, + functionName: "approve", + args: [network.contracts.ADDRESS_DRIVER as OxString, MaxUint256], + }); + + return tokenApprovalTx; + } + + private async _publishMetadataToIpfs( + dripListId: string, + projects: LatestVersion["projects"], + name?: string, + description?: string, + latestVotingRoundId?: string, + ): Promise { + assert( + this._ownerAddress, + `This function requires an active wallet connection.`, + ); + + const dripListMetadata = + this._nftDriverMetadataManager.buildAccountMetadata({ + forAccountId: dripListId, + projects, + name, + description, + latestVotingRoundId, + }); + + const ipfsHash = + await this._nftDriverMetadataManager.pinAccountMetadata(dripListMetadata); + + return ipfsHash; + } + + // We use the count of *all* NFT sub-accounts to generate the salt for the Drip List ID. + // This is because we want to avoid making HTTP requests to the subgraph for each NFT sub-account to check if it's a Drip List. + private _calcSaltFromAddress = ( + address: string, + listCount: number, + ): bigint /* 64bit */ => { + const hash = ethers.keccak256( + ethers.AbiCoder.defaultAbiCoder().encode( + ["string"], + [this.SEED_CONSTANT + address], + ), + ); + const randomBigInt = ethers.toBigInt("0x" + hash.slice(26)); + + let random64BitBigInt = + BigInt(randomBigInt.toString()) & BigInt("0xFFFFFFFFFFFFFFFF"); + + const listCountBigInt = BigInt(listCount); + random64BitBigInt = random64BitBigInt ^ listCountBigInt; + + return random64BitBigInt; + }; +} diff --git a/packages/sdk/src/drips/common/services/GitHub.ts b/packages/sdk/src/drips/common/services/GitHub.ts new file mode 100644 index 0000000..c886c0f --- /dev/null +++ b/packages/sdk/src/drips/common/services/GitHub.ts @@ -0,0 +1,68 @@ +import network from "../wallet/network.js"; +import type { Octokit } from "@octokit/rest"; +import { Buffer } from "buffer"; +import { get } from "../store/mock.js"; + +export default class GitHub { + private octokit: Octokit; + + constructor(octokit: Octokit) { + this.octokit = octokit; + } + + public async getRepoByOwnerAndName(owner: string, repo: string) { + const { data } = await this.octokit.request("GET /repos/{owner}/{repo}", { + owner, + repo, + }); + + return data; + } + + public async getRepoByUrl(repoUrl: string) { + const url = new URL(repoUrl); + + if (url.host !== "github.com") { + throw new Error(`Invalid host: ${url.host}`); + } + + const [, owner, repo] = url.pathname.split("/"); + + return this.getRepoByOwnerAndName(owner, repo); + } + + public async verifyFundingJson(owner: string, repo: string): Promise { + const { data } = await this.octokit.repos + .getContent({ + owner, + repo, + path: "FUNDING.json", + request: { + cache: "reload", + }, + }) + .catch(() => { + throw new Error("FUNDING.json not found."); + }); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const fileContent = Buffer.from((data as any).content, "base64").toString( + "utf-8", + ); + + const fundingJson = JSON.parse(fileContent); + + const fundingJsonOwner = + fundingJson.drips?.[ + network.name === "homestead" ? "ethereum" : network.name + ].ownedBy; + + const { address: expectedOwner } = await get(); + + if (fundingJsonOwner.toLowerCase() !== expectedOwner?.toLowerCase()) { + throw new Error( + "Invalid FUNDING.json file. Does it have the correct Ethereum address?", + ); + } + } +} diff --git a/packages/sdk/src/drips/common/store/mock.ts b/packages/sdk/src/drips/common/store/mock.ts new file mode 100644 index 0000000..d33a80e --- /dev/null +++ b/packages/sdk/src/drips/common/store/mock.ts @@ -0,0 +1,38 @@ +import { mockWalletStore } from "&/drips/common/wallet/mock.js"; + +class WalletSingleton { + private constructor( + private result: Awaited< + ReturnType>["initialize"]> + >, + ) {} + + static async create() { + const mock = mockWalletStore(); + return new WalletSingleton(await mock.initialize()); + } + + get() { + return this.result; + } +} + +async function* getWallet() { + const singleton = await WalletSingleton.create(); + + while (true) { + yield singleton.get(); + } +} + +const generator = getWallet(); + +// TODO: Replicate different store elements +export const get = async () => { + const { value } = await generator.next(); + if (!value) { + throw new Error("No value"); + } + + return value; +}; diff --git a/packages/sdk/src/drips/common/types.ts b/packages/sdk/src/drips/common/types.ts new file mode 100644 index 0000000..7dca4b4 --- /dev/null +++ b/packages/sdk/src/drips/common/types.ts @@ -0,0 +1,132 @@ +export class AddItemError extends Error { + constructor( + message: string, + public severity: "warning" | "error", + public submessage?: string, + public suberrors?: Array, + ) { + super(message); + } +} + +export class AddItemSuberror extends Error { + constructor( + message: string, + public lineNumber: number, + ) { + super(message); + } +} + +type BaseItem = { + rightComponent?: { + component: never; + props: Record; + }; +}; + +type ProjectItem = BaseItem & { + type: "project"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + project: any; +}; + +type DripListItem = BaseItem & { + type: "drip-list"; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dripList: any; +}; + +type EthAddressItem = BaseItem & { + type: "address"; + address: string; +}; + +export type ListEditorItem = ProjectItem | DripListItem | EthAddressItem; + +export type AccountId = string; + +export type Weights = Record; +export type Items = Record; + +export interface DripListConfig { + items: Items; + weights: Weights; + title: string; + description: string | undefined; +} + +export interface State { + dripList: DripListConfig; + recipientErrors: Array; + /** 1 is immediate DL creation, 2 is creating a draft / voting round */ + selectedCreationMode: 1 | 2 | undefined; + /** 1 is Continuous Support, 2 is one-time donation */ + selectedSupportOption: 1 | 2 | undefined; + continuousSupportConfig: { + listSelected: string[]; + streamRateValueParsed?: bigint | undefined; + topUpAmountValueParsed?: bigint | undefined; + }; + oneTimeDonationConfig: { + selectedTokenAddress: string[] | undefined; + amountInputValue: string; + topUpMax: boolean; + amount: bigint | undefined; + }; + votingRoundConfig: { + collaborators: Items; + votingEnds: Date | undefined; + areVotesPrivate: boolean; + areRecipientsRestricted: boolean; + allowedRecipients: Items; + }; + newVotingRoundId: string | undefined; + dripListId: string | undefined; +} + +export type Address = string; +export type IpfsHash = string; + +export type OxString = `0x${string}`; + +export type MetadataKeyValue = { + key: OxString; + value: OxString; +}; + +export type UnwrappedEthersResult = T extends [infer U] + ? U + : T extends readonly [infer U] + ? U + : T; + +export type SplitsReceiver = { + accountId: string; + weight: number; +}; + +export type CallerCall = { + target: OxString; + data: OxString; + value: bigint; +}; + +export type StreamConfig = { + /** An arbitrary number used to identify a drip. When setting a config, it must be greater than or equal to `0`. It's a part of the configuration but the protocol doesn't use it. */ + dripId: bigint; + + /** The UNIX timestamp (in seconds) when dripping should start. When setting a config, it must be greater than or equal to `0`. If set to `0`, the contract will use the timestamp when drips are configured. */ + start: bigint; + + /** The duration (in seconds) of dripping. When setting a config, it must be greater than or equal to `0`. If set to `0`, the contract will drip until the balance runs out. */ + duration: bigint; + + /** The amount per second being dripped. When setting a config, it must be in the smallest unit (e.g., Wei), greater than `0` **and be multiplied by `10 ^ 9`** (`constants.AMT_PER_SEC_MULTIPLIER`). */ + amountPerSec: bigint; +}; + +export type StreamReceiver = { + accountId: bigint; + config: bigint; +}; diff --git a/packages/sdk/src/drips/common/utils/contract-constants.ts b/packages/sdk/src/drips/common/utils/contract-constants.ts new file mode 100644 index 0000000..80db3d8 --- /dev/null +++ b/packages/sdk/src/drips/common/utils/contract-constants.ts @@ -0,0 +1,15 @@ +import { encodeBytes32String } from "ethers"; + +const ASSOCIATED_APP_KEY = "associatedApp"; + +const contractConstants = { + TOTAL_SPLITS_WEIGHT: 1_000_000, + MAX_DRIPS_RECEIVERS: 100, + MAX_SPLITS_RECEIVERS: 200, + AMT_PER_SEC_MULTIPLIER: 1_000_000_000, + AMT_PER_SEC_EXTRA_DECIMALS: 9, + ASSOCIATED_APP_KEY, + ASSOCIATED_APP_KEY_BYTES: encodeBytes32String(ASSOCIATED_APP_KEY), +}; + +export default contractConstants; diff --git a/packages/sdk/src/drips/common/utils/extract-address-from-account.ts b/packages/sdk/src/drips/common/utils/extract-address-from-account.ts new file mode 100644 index 0000000..d848426 --- /dev/null +++ b/packages/sdk/src/drips/common/utils/extract-address-from-account.ts @@ -0,0 +1,36 @@ +import { getAddress, MaxUint256 } from "ethers"; +import { OxString } from "../types.js"; +import isAddressDriverId from "./is-address-driver-id.js"; + +export default function extractAddressFromAccountId( + accountId: string, +): OxString { + if (!accountId) { + throw new Error(`Could not get user address: accountId is missing.`); + } + + const accountIdAsBn = BigInt(accountId); + + if (accountIdAsBn < 0 || accountIdAsBn > MaxUint256) { + throw new Error( + `Could not get user address: ${accountId} is not a valid positive number within the range of a uint256.`, + ); + } + + if (isAddressDriverId(accountId)) { + const mid64BitsMask = (BigInt(2) ** BigInt(64) - BigInt(1)) << BigInt(160); + + if ((accountIdAsBn & mid64BitsMask) !== BigInt(0)) { + throw new Error( + `Could not get user address: ${accountId} is not a valid user ID. The first 64 (after first 32) bits must be 0.`, + ); + } + } + + const mask = BigInt(2) ** BigInt(160) - BigInt(1); + const address = accountIdAsBn & mask; + + const paddedAddress = address.toString(16).padStart(40, "0").toLowerCase(); + + return getAddress(`0x${paddedAddress}`) as OxString; +} diff --git a/packages/sdk/src/drips/common/utils/extract-driver-id.ts b/packages/sdk/src/drips/common/utils/extract-driver-id.ts new file mode 100644 index 0000000..f4f65bb --- /dev/null +++ b/packages/sdk/src/drips/common/utils/extract-driver-id.ts @@ -0,0 +1,32 @@ +export function extractDriverNameFromAccountId( + id: string, +): "address" | "nft" | "immutableSplits" | "repo" { + if (Number.isNaN(Number(id))) { + throw new Error(`Could not get bits: ${id} is not a number.`); + } + + const accountIdAsBigInt = BigInt(id); + + if (accountIdAsBigInt < 0n || accountIdAsBigInt > 2n ** 256n - 1n) { + throw new Error( + `Could not get bits: ${id} is not a valid positive number within the range of a uint256.`, + ); + } + + const mask = 2n ** 32n - 1n; // 32 bits mask + + const bits = (accountIdAsBigInt >> 224n) & mask; + + switch (bits) { + case 0n: + return "address"; + case 1n: + return "nft"; + case 2n: + return "immutableSplits"; + case 3n: + return "repo"; + default: + throw new Error(`Unknown driver for ID ${id}.`); + } +} diff --git a/packages/sdk/src/drips/common/utils/filter-current-chain-data.ts b/packages/sdk/src/drips/common/utils/filter-current-chain-data.ts new file mode 100644 index 0000000..22a9e4a --- /dev/null +++ b/packages/sdk/src/drips/common/utils/filter-current-chain-data.ts @@ -0,0 +1,48 @@ +import assert from "assert"; +import network, { SupportedChain } from "../wallet/network.js"; +import isClaimed, { isUnclaimed } from "./is-claimed.js"; + +function isProjectData(data: { + __typename: string; +}): data is { __typename: "ClaimedProjectData" | "UnClaimedProjectData" } { + return ( + data.__typename === "ClaimedProjectData" || + data.__typename === "UnClaimedProjectData" + ); +} + +export default function filterCurrentChainData< + T extends + | { __typename: string; chain: SupportedChain } + | { __typename: string; chain: SupportedChain }, + /** If filtering project chain data, use this to enforce a claimed / unclaimed status. Must be undefined if not project data. */ + CT extends "claimed" | "unclaimed" | undefined, +>( + items: T[], + expectedProjectStatus?: CT, +): CT extends "claimed" + ? T & { __typename: "ClaimedProjectData" } + : CT extends "unclaimed" + ? T & { __typename: "UnClaimedProjectData" } + : T { + const filteredItems = items.filter((item) => item.chain === network.gqlName); + const item = filteredItems[0]; + + assert(item, "Expected project data for current chain"); + + if (expectedProjectStatus) { + assert(isProjectData(item), "Expected project data"); + assert( + expectedProjectStatus === "unclaimed" + ? isUnclaimed(item) + : isClaimed(item), + `Expected ${expectedProjectStatus} project data`, + ); + } + + return item as CT extends "claimed" + ? T & { __typename: "ClaimedProjectData" } + : CT extends "unclaimed" + ? T & { __typename: "UnClaimedProjectData" } + : T; +} diff --git a/packages/sdk/src/drips/common/utils/format-split-receivers.ts b/packages/sdk/src/drips/common/utils/format-split-receivers.ts new file mode 100644 index 0000000..54afef0 --- /dev/null +++ b/packages/sdk/src/drips/common/utils/format-split-receivers.ts @@ -0,0 +1,33 @@ +import { toBigInt } from "ethers"; +import type { SplitsReceiver } from "&/drips/common/types.js"; + +export function formatSplitReceivers(receivers: SplitsReceiver[]) { + // Splits receivers must be sorted by user ID, deduplicated, and without weights <= 0. + + const uniqueReceivers = receivers.reduce((unique: SplitsReceiver[], o) => { + if ( + !unique.some( + (obj: SplitsReceiver) => + obj.accountId === o.accountId && obj.weight === o.weight, + ) + ) { + unique.push(o); + } + return unique; + }, []); + + const sortedReceivers = uniqueReceivers.sort((a, b) => + // Sort by user ID. + + BigInt(a.accountId) > BigInt(b.accountId) + ? 1 + : BigInt(a.accountId) < BigInt(b.accountId) + ? -1 + : 0, + ); + + return sortedReceivers.map((r) => ({ + accountId: toBigInt(r.accountId), + weight: r.weight, + })); +} diff --git a/packages/sdk/src/drips/common/utils/format-stream-receivers.ts b/packages/sdk/src/drips/common/utils/format-stream-receivers.ts new file mode 100644 index 0000000..d65c745 --- /dev/null +++ b/packages/sdk/src/drips/common/utils/format-stream-receivers.ts @@ -0,0 +1,33 @@ +import type { StreamReceiver } from "&/drips/common/types.js"; + +export const formatStreamReceivers = (receivers: StreamReceiver[]) => { + // Drips receivers must be sorted by user ID and config, deduplicated, and without amount per second <= 0. + + const uniqueReceivers = receivers.reduce((unique: StreamReceiver[], o) => { + if ( + !unique.some( + (obj: StreamReceiver) => + obj.accountId === o.accountId && obj.config === o.config, + ) + ) { + unique.push(o); + } + return unique; + }, []); + + const sortedReceivers = uniqueReceivers + // Sort by accountId. + .sort((a, b) => + a.accountId > b.accountId + ? 1 + : a.accountId < b.accountId + ? -1 + : // Sort by config. + a.config > b.config + ? 1 + : a.config < b.config + ? -1 + : 0, + ); + return sortedReceivers; +}; diff --git a/packages/sdk/src/drips/common/utils/get-own-account-id.ts b/packages/sdk/src/drips/common/utils/get-own-account-id.ts new file mode 100644 index 0000000..a608e2a --- /dev/null +++ b/packages/sdk/src/drips/common/utils/get-own-account-id.ts @@ -0,0 +1,16 @@ +import assert from "assert"; +import { executeAddressDriverReadMethod } from "&/drips/common/drivers/address-driver.js"; +import { get } from "&/drips/common/store/mock.js"; +import { OxString } from "&/drips/common/types.js"; + +export default async function getOwnAccountId() { + const { signer } = await get(); + assert(signer, `'getOwnAccountId' requires a signer but it's missing.`); + + return ( + await executeAddressDriverReadMethod({ + functionName: "calcAccountId", + args: [signer.address as OxString], + }) + ).toString(); +} diff --git a/packages/sdk/src/drips/common/utils/ipfs.ts b/packages/sdk/src/drips/common/utils/ipfs.ts new file mode 100644 index 0000000..d63b03a --- /dev/null +++ b/packages/sdk/src/drips/common/utils/ipfs.ts @@ -0,0 +1,20 @@ +const PUBLIC_PINATA_GATEWAY_URL = "https://gateway.pinata.cloud"; + +export async function fetchIpfs(hash: string, f = fetch) { + return f(`${PUBLIC_PINATA_GATEWAY_URL}/ipfs/${hash}`); +} + +export async function pin(data: Record, f = fetch) { + const res = await f("/api/ipfs/pin", { + method: "POST", + body: JSON.stringify(data, (_, value) => + typeof value === "bigint" ? value.toString() : value, + ), + }); + + if (!res.ok) { + throw new Error(`Pinning account metadata failed: ${await res.text()}`); + } + + return res.text(); +} diff --git a/packages/sdk/src/drips/common/utils/is-address-driver-id.ts b/packages/sdk/src/drips/common/utils/is-address-driver-id.ts new file mode 100644 index 0000000..263ca18 --- /dev/null +++ b/packages/sdk/src/drips/common/utils/is-address-driver-id.ts @@ -0,0 +1,18 @@ +import { extractDriverNameFromAccountId } from "./extract-driver-id.js"; + +export type AddressDriverId = string; + +export default function isAddressDriverId( + idAsString: string, +): idAsString is AddressDriverId { + const isNaN = Number.isNaN(Number(idAsString)); + + const isAccountIdOfAddressDriver = + extractDriverNameFromAccountId(idAsString) === "address"; + + if (isNaN || !isAccountIdOfAddressDriver) { + return false; + } + + return true; +} diff --git a/packages/sdk/src/drips/common/utils/is-claimed.ts b/packages/sdk/src/drips/common/utils/is-claimed.ts new file mode 100644 index 0000000..8b36566 --- /dev/null +++ b/packages/sdk/src/drips/common/utils/is-claimed.ts @@ -0,0 +1,23 @@ +export default function isClaimed< + IT extends + | { + __typename: "ClaimedProjectData"; + } + | { + __typename: "UnClaimedProjectData"; + }, +>(chainData: IT): chainData is IT & { __typename: "ClaimedProjectData" } { + return chainData.__typename === "ClaimedProjectData"; +} + +export function isUnclaimed( + chainData: + | { + __typename: "ClaimedProjectData"; + } + | { + __typename: "UnClaimedProjectData"; + }, +): chainData is { __typename: "UnClaimedProjectData" } { + return chainData.__typename === "UnClaimedProjectData"; +} diff --git a/packages/sdk/src/drips/common/utils/key-value-to-metadata.ts b/packages/sdk/src/drips/common/utils/key-value-to-metadata.ts new file mode 100644 index 0000000..9ab5fd0 --- /dev/null +++ b/packages/sdk/src/drips/common/utils/key-value-to-metadata.ts @@ -0,0 +1,15 @@ +import { encodeBytes32String, hexlify, toUtf8Bytes } from "ethers"; +import type { MetadataKeyValue, OxString } from "&/drips/common/types.js"; + +export default function keyValueToMetatada({ + key, + value, +}: { + key: string; + value: string; +}): MetadataKeyValue { + return { + key: encodeBytes32String(key) as OxString, + value: hexlify(toUtf8Bytes(value)) as OxString, + }; +} diff --git a/packages/sdk/src/drips/common/utils/make-stream.ts b/packages/sdk/src/drips/common/utils/make-stream.ts new file mode 100644 index 0000000..30ef5ed --- /dev/null +++ b/packages/sdk/src/drips/common/utils/make-stream.ts @@ -0,0 +1,61 @@ +import assert from "node:assert"; +import { isAddress } from "ethers"; + +const numericTest = /^\d+$/; + +/** + * Create a globally unique Stream ID string, including the stream's sender user ID and the associated receiver's + * dripId, as well as the token address. + * @param senderAccountId The stream sender's accountId. + * @param tokenAddress The token address of the currency the stream is in. + * @param dripId The dripId of the stream's associated receiver. + * @returns The stream ID string. + */ +export default function makeStreamId( + senderAccountId: string, + tokenAddress: string, + dripId: string, +) { + if ( + !( + numericTest.test(senderAccountId) && + numericTest.test(dripId) && + isAddress(tokenAddress) + ) + ) { + throw new Error("Invalid values"); + } + + return `${senderAccountId}-${tokenAddress.toLowerCase()}-${dripId}`; +} + +/** + * Given a stream ID created with `makeStreamId`, decode it into its three parts; the sender's user ID, the token + * address and the dripId of the on-chain receiver. + * @param streamId The stream ID to decode. + * @returns An object including the stream's sender user ID, the token address of the token the stream is streaming, + * and the on-chain dripId. + */ +export function decodeStreamId(streamId: string) { + const parts = streamId.split("-"); + + assert(parts.length === 3, "Invalid stream ID"); + + const values = { + senderAccountId: parts[0], + tokenAddress: parts[1].toLowerCase(), + dripId: parts[2], + }; + + if ( + !( + numericTest.test(values.senderAccountId) && + numericTest.test(values.dripId) && + isAddress(values.tokenAddress) + ) + ) { + throw new Error("Invalid stream ID"); + } + + return values; +} diff --git a/packages/sdk/src/drips/common/utils/random-big-until.ts b/packages/sdk/src/drips/common/utils/random-big-until.ts new file mode 100644 index 0000000..090ea63 --- /dev/null +++ b/packages/sdk/src/drips/common/utils/random-big-until.ts @@ -0,0 +1,18 @@ +import { randomBytes, toBigInt } from "ethers"; + +/** + * Keeps generating a random bigint with a length of `bytes`, until the result + * is no longer included in an array of `existing` bigints. + * @param existing The array of bigints to ensure uniqueness against. + * @param bytes The amount of bytes to generate the random bigint with. + * @returns The random bigint, which is guaranteed to be unique against values in `existing`. + */ +export default function (existing: bigint[], bytes: number) { + let result: bigint | undefined = undefined; + + while (!result || existing.includes(result)) { + result = toBigInt(randomBytes(bytes)); + } + + return result; +} diff --git a/packages/sdk/src/drips/common/utils/settlement-date.ts b/packages/sdk/src/drips/common/utils/settlement-date.ts new file mode 100644 index 0000000..54ebd92 --- /dev/null +++ b/packages/sdk/src/drips/common/utils/settlement-date.ts @@ -0,0 +1,34 @@ +export function nextMainnetSettlementDate() { + const currentDate = new Date(); + + let lastDay = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + 1, + 0, + ); + + if (lastDay.getDay() < 4) { + lastDay.setDate(lastDay.getDate() - 7); + } + + lastDay.setDate(lastDay.getDate() - (lastDay.getDay() - 4)); + + if (currentDate > lastDay) { + lastDay = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + 2, + 0, + ); + if (lastDay.getDay() < 4) { + lastDay.setDate(lastDay.getDate() - 7); + } + lastDay.setDate(lastDay.getDate() - (lastDay.getDay() - 4)); + } + + return lastDay; +} + +export function nextFilecoinSettlementDate() { + // Filecoin settlement happens daily + return new Date(); +} diff --git a/packages/sdk/src/drips/common/utils/stream-config.ts b/packages/sdk/src/drips/common/utils/stream-config.ts new file mode 100644 index 0000000..91ec952 --- /dev/null +++ b/packages/sdk/src/drips/common/utils/stream-config.ts @@ -0,0 +1,85 @@ +import type { StreamConfig } from "&/drips/common/types.js"; + +export const validateStreamConfig = (streamConfig: StreamConfig): void => { + const { dripId, start, duration, amountPerSec } = streamConfig; + + if (dripId < 0) { + throw new Error(`'dripId' must be greater than or equal to 0.`); + } + + if (start < 0) { + throw new Error(`'start' must be greater than or equal to 0.`); + } + + if (duration < 0) { + throw new Error(`'duration' must be greater than or equal to 0.`); + } + + if (amountPerSec <= 0) { + throw new Error(`'amountPerSec' must be greater than 0.`); + } +}; + +export function streamConfigToUint256(streamConfig: StreamConfig): bigint { + const configAsBigInt = toUint256(streamConfig); + + const { dripId, start, duration, amountPerSec } = streamConfig; + if (dripId !== fromUint256(configAsBigInt).dripId) { + throw new Error(`'dripId' does not match.`); + } + if (start !== fromUint256(configAsBigInt).start) { + throw new Error(`'start' does not match.`); + } + if (duration !== fromUint256(configAsBigInt).duration) { + throw new Error(`'duration' does not match.`); + } + if (amountPerSec !== fromUint256(configAsBigInt).amountPerSec) { + throw new Error(`'amountPerSec' does not match.`); + } + + return configAsBigInt; +} + +export function toUint256(streamConfig: StreamConfig): bigint { + validateStreamConfig(streamConfig); + + const { dripId, start, duration, amountPerSec } = streamConfig; + + let config = BigInt(dripId); + config = (config << 160n) | BigInt(amountPerSec); + config = (config << 32n) | BigInt(start); + config = (config << 32n) | BigInt(duration); + + return config; +} + +export function streamConfigFromUint256(streamConfig: bigint): StreamConfig { + const config = fromUint256(streamConfig); + + if (toUint256(config) !== streamConfig) { + throw new Error(`'dripId' does not match.`); + } + + return config; +} + +export function fromUint256(streamConfig: bigint): StreamConfig { + const mask32 = (1n << 32n) - 1n; + const mask160 = (1n << 160n) - 1n; + + const dripId = streamConfig >> (160n + 32n + 32n); + const amountPerSec = (streamConfig >> (32n + 32n)) & mask160; + const start = (streamConfig >> 32n) & mask32; + const duration = streamConfig & mask32; + + const config: StreamConfig = { + dripId: dripId, + amountPerSec: amountPerSec, + duration: duration, + start: start, + }; + + validateStreamConfig(config); + + return config; +} diff --git a/packages/sdk/src/drips/common/utils/streams.ts b/packages/sdk/src/drips/common/utils/streams.ts new file mode 100644 index 0000000..a17d435 --- /dev/null +++ b/packages/sdk/src/drips/common/utils/streams.ts @@ -0,0 +1,294 @@ +import { Signer, toBigInt } from "ethers"; +import { OxString } from "../types.js"; +import getOwnAccountId from "./get-own-account-id.js"; +import keyValueToMetatada from "./key-value-to-metadata.js"; +import { gql } from "graphql-request"; +import query from "../graphql/dripQL.js"; +import { addressDriverAccountMetadataParser } from "../schemas/index.js"; +import network from "../wallet/network.js"; +import filterCurrentChainData from "./filter-current-chain-data.js"; +import randomBigintUntilUnique from "./random-big-until.js"; +import { + streamConfigFromUint256, + streamConfigToUint256, +} from "./stream-config.js"; +import { pin } from "./ipfs.js"; +import populateNewStreamFlowTxs from "../drivers/populate-create-new-stream-flow-txs.js"; +import extractAddressFromAccountId from "./extract-address-from-account.js"; +import { extractDriverNameFromAccountId } from "./extract-driver-id.js"; +import makeStreamId from "./make-stream.js"; + +type NewStreamOptions = { + tokenAddress: string; + amountPerSecond: bigint; + recipientAccountId: string; + name: string | undefined; + startAt?: Date; + durationSeconds?: number; +}; + +const METADATA_PARSER = addressDriverAccountMetadataParser; +const USER_METADATA_KEY = "ipfs"; + +export async function _getCurrentStreamsAndReceivers( + accountId: string, + tokenAddress: string, +) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const currentStreamsQueryRes = await query( + gql` + query CurrentStreams($userAccountId: ID!, $chains: [SupportedChain!]) { + userById(accountId: $userAccountId, chains: $chains) { + chainData { + chain + streams { + outgoing { + id + name + isPaused + config { + raw + amountPerSecond { + tokenAddress + } + dripId + amountPerSecond { + amount + } + durationSeconds + startDate + } + receiver { + ... on User { + account { + accountId + } + } + ... on DripList { + account { + accountId + } + } + } + } + } + } + } + } + `, + { + userAccountId: accountId, + chains: [network.gqlName], + }, + ); + + const chainData = filterCurrentChainData( + currentStreamsQueryRes.userById.chainData, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) as any; + + const { outgoing: currentStreams } = chainData.streams; + + const currentReceivers = currentStreams + .filter( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (stream: any) => + stream.config.amountPerSecond.tokenAddress.toLowerCase() === + tokenAddress.toLowerCase(), + ) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .filter((stream: any) => !stream.isPaused) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .map((stream: any) => ({ + accountId: stream.receiver.account.accountId, + config: stream.config.raw, + })); + + return { + currentStreams, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + currentReceivers: currentReceivers.map((r: any) => ({ + accountId: toBigInt(r.accountId), + config: toBigInt(r.config), + })), + }; +} + +export async function buildStreamCreateBatchTx( + signer: Signer, + streamOptions: NewStreamOptions, + topUpAmount?: bigint, +) { + const ownAccountId = await getOwnAccountId(); + + const { currentStreams, currentReceivers } = + await _getCurrentStreamsAndReceivers( + ownAccountId, + streamOptions.tokenAddress, + ); + + const newStreamDripId = randomBigintUntilUnique( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + currentReceivers.map((r: any) => streamConfigFromUint256(r.config).dripId), + 4, + ); + + const { + startAt: scheduleStartAt, + durationSeconds: scheduleDurationSeconds, + amountPerSecond: amountPerSec, + } = streamOptions; + + const newStreamConfig = streamConfigToUint256({ + dripId: newStreamDripId, + start: BigInt(scheduleStartAt?.getTime() ?? 0) / 1000n, + duration: BigInt(scheduleDurationSeconds ?? 0), + amountPerSec, + }); + + const newMetadata = _buildMetadata(currentStreams, ownAccountId, { + ...streamOptions, + dripId: newStreamDripId, + }); + + const newHash = await pin(newMetadata); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const currentRec = currentReceivers.map((r: any) => ({ + accountId: r.accountId, + config: streamConfigFromUint256(r.config), + })); + + return { + newHash, + batch: await populateNewStreamFlowTxs({ + tokenAddress: streamOptions.tokenAddress as OxString, + currentReceivers: currentRec, + newReceivers: [ + ...currentRec, + { + config: streamConfigFromUint256(newStreamConfig), + accountId: toBigInt(streamOptions.recipientAccountId), + }, + ], + accountMetadata: [ + keyValueToMetatada({ + key: USER_METADATA_KEY, + value: newHash, + }), + ], + balanceDelta: topUpAmount ?? 0n, + transferToAddress: extractAddressFromAccountId(ownAccountId), + }), + }; +} + +function _buildMetadata( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + streams: any[], + accountId: string, + newStream?: NewStreamOptions & { dripId: bigint }, +) { + const streamsByTokenAddress = streams.reduce( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (acc: any, stream: any) => ({ + ...acc, + [stream.config.amountPerSecond.tokenAddress.toLowerCase()]: [ + ...(acc[stream.config.amountPerSecond.tokenAddress.toLowerCase()] ?? + []), + stream, + ], + }), + {}, + ); + + const newStreamsByTokenAddress = newStream + ? { + ...streamsByTokenAddress, + [newStream.tokenAddress.toLowerCase()]: [ + ...(streamsByTokenAddress[newStream.tokenAddress.toLowerCase()] ?? + []), + { + id: makeStreamId( + accountId, + newStream.tokenAddress, + newStream.dripId.toString(), + ), + name: newStream.name, + config: { + raw: streamConfigToUint256({ + dripId: newStream.dripId, + start: BigInt(newStream.startAt?.getTime() ?? 0) / 1000n, + duration: BigInt(newStream.durationSeconds ?? 0), + amountPerSec: newStream.amountPerSecond, + }).toString(), + dripId: newStream.dripId.toString(), + amountPerSecond: { + amount: newStream.amountPerSecond.toString(), + }, + durationSeconds: newStream.durationSeconds, + startDate: + newStream.startAt?.toISOString() ?? new Date().toISOString(), + }, + receiver: { + account: { + accountId: newStream.recipientAccountId, + }, + }, + }, + ], + } + : streamsByTokenAddress; + + // Parsing with the latest parser version to ensure we never write any invalid metadata. + return METADATA_PARSER.parseLatest({ + describes: { + driver: "address", + accountId, + }, + assetConfigs: Object.entries(newStreamsByTokenAddress).map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ([tokenAddress, streams]: [string, any]) => { + return { + tokenAddress, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + streams: streams.map((stream: any) => { + const recipientDriver = extractDriverNameFromAccountId( + stream.receiver.account.accountId, + ); + + let supportedDriver: "address" | "nft" | "repo"; + + if (["address", "nft", "repo"].includes(recipientDriver)) { + supportedDriver = recipientDriver as "address" | "nft" | "repo"; + } else { + throw new Error( + `Unsupported recipient driver: ${recipientDriver}`, + ); + } + + return { + id: stream.id, + initialDripsConfig: { + raw: stream.config.raw, + dripId: stream.config.dripId, + amountPerSecond: BigInt(stream.config.amountPerSecond.amount), + durationSeconds: stream.config.durationSeconds || 0, + startTimestamp: + new Date(stream.config.startDate).getTime() / 1000, + }, + receiver: { + driver: supportedDriver, + accountId: stream.receiver.account.accountId, + }, + archived: false, + name: stream.name ?? undefined, + }; + }), + }; + }, + ), + timestamp: Math.floor(new Date().getTime() / 1000), + writtenByAddress: extractAddressFromAccountId(accountId), + }); +} diff --git a/packages/sdk/src/drips/common/utils/tx-to-caller-call.ts b/packages/sdk/src/drips/common/utils/tx-to-caller-call.ts new file mode 100644 index 0000000..b5ff6aa --- /dev/null +++ b/packages/sdk/src/drips/common/utils/tx-to-caller-call.ts @@ -0,0 +1,10 @@ +import { type ContractTransaction, toBigInt } from "ethers"; +import type { CallerCall, OxString } from "&/drips/common/types.js"; + +export default function txToCallerCall(tx: ContractTransaction): CallerCall { + return { + target: tx.to as OxString, + data: tx.data as OxString, + value: toBigInt(tx.value ?? 0), + }; +} diff --git a/packages/sdk/src/drips/common/utils/tx-to-safe-drips.ts b/packages/sdk/src/drips/common/utils/tx-to-safe-drips.ts new file mode 100644 index 0000000..317897f --- /dev/null +++ b/packages/sdk/src/drips/common/utils/tx-to-safe-drips.ts @@ -0,0 +1,12 @@ +import type { ContractTransaction } from "ethers"; + +// TODO: this was copied from the SDK. Check if it's still needed. +export default function txToSafeDripsTx( + tx: ContractTransaction, +): ContractTransaction { + if (!tx.value) { + tx.value = 0n; + } + + return tx; +} diff --git a/packages/sdk/src/drips/common/utils/unwrap-ether-result.ts b/packages/sdk/src/drips/common/utils/unwrap-ether-result.ts new file mode 100644 index 0000000..a53120e --- /dev/null +++ b/packages/sdk/src/drips/common/utils/unwrap-ether-result.ts @@ -0,0 +1,11 @@ +import type { UnwrappedEthersResult } from "&/drips/common/types.js"; + +export default function unwrapEthersResult( + result: T | T[], +): UnwrappedEthersResult | UnwrappedEthersResult { + if (Array.isArray(result) && result.length === 1) { + return result[0] as UnwrappedEthersResult; + } + + return result as UnwrappedEthersResult; +} diff --git a/packages/sdk/src/drips/common/wallet/FailOverProvider.ts b/packages/sdk/src/drips/common/wallet/FailOverProvider.ts new file mode 100644 index 0000000..d683d30 --- /dev/null +++ b/packages/sdk/src/drips/common/wallet/FailOverProvider.ts @@ -0,0 +1,108 @@ +import type { + JsonRpcApiProviderOptions, + JsonRpcPayload, + JsonRpcResult, + Networkish, +} from "ethers"; +import { FetchRequest, JsonRpcProvider } from "ethers"; + +class AggregatedRpcError extends Error { + public readonly errors: { rpcEndpoint: string; error: unknown }[]; + + constructor(errors: { rpcEndpoint: string; error: unknown }[]) { + super( + `All RPC endpoints failed:\n${errors + .map( + (e) => + `Endpoint '${e.rpcEndpoint}' failed with error: ${ + (e.error as Error).message + }.`, + ) + .join("\n")}`, + ); + + this.name = "AggregatedRpcError"; + this.errors = errors; + } +} + +/** + * A `JsonRpcProvider` that transparently fails over to a list of backup JSON-RPC endpoints. + */ +export default class FailoverJsonRpcProvider extends JsonRpcProvider { + private readonly _rpcEndpoints: (string | FetchRequest)[]; + + /** + * @param rpcEndpoints An array of JSON-RPC endpoints. The order determines the failover order. + */ + constructor( + rpcEndpoints: (string | FetchRequest)[], + network?: Networkish, + options?: JsonRpcApiProviderOptions, + ) { + super(rpcEndpoints[0], network, options); + + this._rpcEndpoints = rpcEndpoints; + } + + /** + * Overrides the `_send` method to try each endpoint until the request succeeds. + * + * @param payload - The JSON-RPC payload or array of payloads to send. + * @returns A promise that resolves to the result of the first successful JSON-RPC call. + */ + public override async _send( + payload: JsonRpcPayload | Array, + ): Promise> { + // The actual sending of the request is the same as in the base class. + // The only difference is that we're creating a new `FetchRequest` for each endpoint, + // instead of getting the `request` from `_getConnection()`, which will return the *primary* (first) endpoint. + + const errors: { rpcEndpoint: string; error: unknown }[] = []; + + // Try each endpoint, in order. + for (const rpcEndpoint of this._rpcEndpoints) { + try { + let request: FetchRequest; + + if (typeof rpcEndpoint === "string") { + request = new FetchRequest(rpcEndpoint); + } else { + request = rpcEndpoint.clone(); + } + + request.body = JSON.stringify(payload); + request.setHeader("content-type", "application/json"); + const response = await request.send(); + response.assertOk(); + + let resp = response.bodyJson; + if (!Array.isArray(resp)) { + resp = [resp]; + } + + return resp; + } catch (error: unknown) { + const endpointUrl = this._getRpcEndpointUrl(rpcEndpoint); + errors.push({ rpcEndpoint: endpointUrl, error }); + } + } + + throw new AggregatedRpcError(errors); + } + + /** + * Returns a copy of the endpoint URLs used by the provider. + */ + public getRpcEndpointUrls(): string[] { + return this._rpcEndpoints.map(this._getRpcEndpointUrl); + } + + private _getRpcEndpointUrl(rpcEndpoint: string | FetchRequest): string { + if (typeof rpcEndpoint === "string") { + return rpcEndpoint; + } + + return rpcEndpoint.url; + } +} diff --git a/packages/sdk/src/drips/common/wallet/mock.ts b/packages/sdk/src/drips/common/wallet/mock.ts new file mode 100644 index 0000000..cfd2424 --- /dev/null +++ b/packages/sdk/src/drips/common/wallet/mock.ts @@ -0,0 +1,65 @@ +import testnetMockProvider from "&/drips/common/wallet/provider.js"; +import type { BrowserProvider, JsonRpcProvider, JsonRpcSigner } from "ethers"; +import FailoverJsonRpcProvider from "&/drips/common/wallet/FailOverProvider.js"; +import { getNetwork, type Network } from "&/drips/common/wallet/network.js"; +import type { OxString } from "&/drips/common/types.js"; +import { executeAddressDriverReadMethod } from "&/drips/common/drivers/address-driver.js"; + +export interface ConnectedWalletStoreState { + connected: true; + address: string; + dripsAccountId: string; + provider: BrowserProvider | JsonRpcProvider | FailoverJsonRpcProvider; + signer: JsonRpcSigner; + network: Network; +} + +export interface DisconnectedWalletStoreState { + connected: false; + network: Network; + provider: BrowserProvider | JsonRpcProvider | FailoverJsonRpcProvider; + dripsAccountId?: undefined; + address?: undefined; + signer?: undefined; + safe?: never; +} + +export type WalletStoreState = + | ConnectedWalletStoreState + | DisconnectedWalletStoreState; + +// TODO: Implement real wallet store +export const mockWalletStore = () => { + // TODO: Change this to a real address + const address = "0x433220a86126eFe2b8C98a723E73eBAd2D0CbaDc"; + const provider = testnetMockProvider(address); + + async function initialize() { + const signer = await provider.getSigner(); + + const ownAccountId = ( + await executeAddressDriverReadMethod({ + functionName: "calcAccountId", + args: [signer.address as OxString], + }) + ).toString(); + + const network = await provider.getNetwork(); + if (network.chainId) { + throw new Error("Network chainId is not defined"); + } + + const chainId = Number(network.chainId); + + return { + connected: true, + address, + provider, + signer, + network: getNetwork(chainId), + dripsAccountId: ownAccountId, + }; + } + + return { initialize }; +}; diff --git a/packages/sdk/src/drips/common/wallet/network.ts b/packages/sdk/src/drips/common/wallet/network.ts new file mode 100644 index 0000000..6e7b8b9 --- /dev/null +++ b/packages/sdk/src/drips/common/wallet/network.ts @@ -0,0 +1,325 @@ +import assert from "node:assert"; +import { getOptionalEnvVar } from "&/drips/common/wallet/provider.js"; +import { + nextFilecoinSettlementDate, + nextMainnetSettlementDate, +} from "&/drips/common/utils/settlement-date.js"; + +export const SUPPORTED_CHAIN_IDS = [ + 1, 80002, 11155420, 11155111, 84532, 314, +] as const; +export type ChainId = (typeof SUPPORTED_CHAIN_IDS)[number]; + +export type AutoUnwrapPair = { + name: string; + nativeSymbol: string; + wrappedSymbol: string; +}; + +export enum SupportedChain { + BaseSepolia = "BASE_SEPOLIA", + Filecoin = "FILECOIN", + Mainnet = "MAINNET", + OptimismSepolia = "OPTIMISM_SEPOLIA", + PolygonAmoy = "POLYGON_AMOY", + Sepolia = "SEPOLIA", +} + +export type Network = { + chainId: ChainId; + name: string; + label: string; + token: string; + id: string; + rpcUrl: string; + fallbackRpcUrl?: string; + color: string; + isTestnet: boolean; + subdomain: string; + gqlName: SupportedChain; + autoUnwrapPairs: AutoUnwrapPair[] | undefined; + displayNetworkPicker: boolean; + applyGasBuffers: boolean; + settlement: { + nextSettlementDate: () => Date; + explainerText: string; + }; + explorer: { + name: string; + linkTemplate: (txHash: string, networkName: string) => string; + }; + contracts: { + ADDRESS_DRIVER: string; + DRIPS: string; + CALLER: string; + REPO_DRIVER: string; + NFT_DRIVER: string; + NATIVE_TOKEN_UNWRAPPER: string | undefined; + }; + /** + * If enabled, LP, blog, and legal routes are redirected to https://drips.network/. + * This will be obsolete once the app goes fully multi-chain, without separate deployments per network. + */ + alternativeChainMode: boolean; +}; + +export type ValueForEachSupportedChain = Record< + (typeof SUPPORTED_CHAIN_IDS)[number], + T +>; + +const etherscanLinkTemplate = (txHash: string, networkName: string) => + networkName === "homestead" + ? `https://etherscan.io/tx/${txHash}` + : `https://${networkName}.etherscan.io/tx/${txHash}`; + +const envBaseUrl = getOptionalEnvVar("PUBLIC_BASE_URL"); +const BASE_URL = envBaseUrl ?? "https://localhost:5173"; + +export const NETWORK_CONFIG: ValueForEachSupportedChain = { + [1]: { + chainId: 1, + name: "homestead", + label: "Ethereum Mainnet", + token: "ETH", + id: "0x1", + rpcUrl: `${BASE_URL}/api/rpc/infura/mainnet`, + fallbackRpcUrl: `${BASE_URL}/api/rpc/alchemy/mainnet`, + + color: "#627EEA", + isTestnet: false, + subdomain: "drips.network", + gqlName: SupportedChain.Mainnet, + autoUnwrapPairs: [ + { + name: "Ethereum", + nativeSymbol: "ETH", + wrappedSymbol: "WETH", + }, + ], + displayNetworkPicker: false, + applyGasBuffers: true, + explorer: { + name: "Etherscan", + linkTemplate: etherscanLinkTemplate, + }, + contracts: { + ADDRESS_DRIVER: "0x1455d9bD6B98f95dd8FEB2b3D60ed825fcef0610", + DRIPS: "0xd0Dd053392db676D57317CD4fe96Fc2cCf42D0b4", + CALLER: "0x60F25ac5F289Dc7F640f948521d486C964A248e5", + REPO_DRIVER: "0x770023d55D09A9C110694827F1a6B32D5c2b373E", + NFT_DRIVER: "0xcf9c49B0962EDb01Cdaa5326299ba85D72405258", + NATIVE_TOKEN_UNWRAPPER: undefined, + }, + settlement: { + nextSettlementDate: nextMainnetSettlementDate, + explainerText: + "Funds from projects, streams and Drip Lists settle and become collectable on the last Thursday of each month.", + }, + alternativeChainMode: false, + }, + [80002]: { + chainId: 80002, + name: "amoy", + label: "Polygon Amoy", + token: "MATIC", + id: "0x13882", + rpcUrl: `${BASE_URL}/api/rpc/infura/polygon-amoy`, + fallbackRpcUrl: `${BASE_URL}/api/rpc/alchemy/polygon-amoy`, + + color: "#627EEA", + isTestnet: true, + subdomain: "amoy.drips.network", + gqlName: SupportedChain.PolygonAmoy, + autoUnwrapPairs: [], + displayNetworkPicker: false, + applyGasBuffers: true, + explorer: { + name: "Etherscan", + linkTemplate: etherscanLinkTemplate, + }, + contracts: { + ADDRESS_DRIVER: "0x004310a6d47893Dd6e443cbE471c24aDA1e6c619", + DRIPS: "0xeebCd570e50fa31bcf6eF10f989429C87C3A6981", + CALLER: "0x5C7c5AA20b15e13229771CB7De36Fe1F54238372", + REPO_DRIVER: "0x54372850Db72915Fd9C5EC745683EB607b4a8642", + NFT_DRIVER: "0xDafd9Ab96E62941808caa115D184D30A200FA777", + NATIVE_TOKEN_UNWRAPPER: undefined, + }, + settlement: { + nextSettlementDate: nextMainnetSettlementDate, + explainerText: + "Funds from projects, streams and Drip Lists settle and become collectable on the last Thursday of each month.", + }, + alternativeChainMode: true, + }, + [11155420]: { + chainId: 11155420, + name: "optimism-sepolia", + label: "OP Sepolia", + token: "ETH", + id: "0xaa37dc", + rpcUrl: `${BASE_URL}/api/rpc/infura/optimism-sepolia`, + fallbackRpcUrl: `${BASE_URL}/api/rpc/alchemy/optimism-sepolia`, + + color: "#627EEA", + isTestnet: true, + subdomain: "optimism-sepolia.drips.network", + gqlName: SupportedChain.OptimismSepolia, + autoUnwrapPairs: [], + displayNetworkPicker: false, + applyGasBuffers: true, + explorer: { + name: "Etherscan", + linkTemplate: etherscanLinkTemplate, + }, + contracts: { + ADDRESS_DRIVER: "0x70E1E1437AeFe8024B6780C94490662b45C3B567", + DRIPS: "0x74A32a38D945b9527524900429b083547DeB9bF4", + CALLER: "0x09e04Cb8168bd0E8773A79Cc2099f19C46776Fee", + REPO_DRIVER: "0xa71bdf410D48d4AA9aE1517A69D7E1Ef0c179b2B", + NFT_DRIVER: "0xdC773a04C0D6EFdb80E7dfF961B6a7B063a28B44", + NATIVE_TOKEN_UNWRAPPER: undefined, + }, + settlement: { + nextSettlementDate: nextMainnetSettlementDate, + explainerText: + "Funds from projects, streams and Drip Lists settle and become collectable on the last Thursday of each month.", + }, + alternativeChainMode: true, + }, + [11155111]: { + chainId: 11155111, + name: "sepolia", + label: "Sepolia", + token: "ETH", + id: "0xaa36a7", + rpcUrl: `${BASE_URL}/api/rpc/infura/sepolia`, + fallbackRpcUrl: `${BASE_URL}/api/rpc/alchemy/sepolia`, + + color: "#627EEA", + isTestnet: true, + subdomain: "sepolia.drips.network", + gqlName: SupportedChain.Sepolia, + autoUnwrapPairs: [], + displayNetworkPicker: false, + applyGasBuffers: true, + explorer: { + name: "Etherscan", + linkTemplate: etherscanLinkTemplate, + }, + contracts: { + ADDRESS_DRIVER: "0x70E1E1437AeFe8024B6780C94490662b45C3B567", + DRIPS: "0x74A32a38D945b9527524900429b083547DeB9bF4", + CALLER: "0x09e04Cb8168bd0E8773A79Cc2099f19C46776Fee", + REPO_DRIVER: "0xa71bdf410D48d4AA9aE1517A69D7E1Ef0c179b2B", + NFT_DRIVER: "0xdC773a04C0D6EFdb80E7dfF961B6a7B063a28B44", + NATIVE_TOKEN_UNWRAPPER: undefined, + }, + settlement: { + nextSettlementDate: nextMainnetSettlementDate, + explainerText: + "Funds from projects, streams and Drip Lists settle and become collectable on the last Thursday of each month.", + }, + alternativeChainMode: false, + }, + [84532]: { + chainId: 84532, + name: "base-sepolia", + label: "Base Sepolia", + token: "ETH", + id: "0x14a34", + rpcUrl: `${BASE_URL}/api/rpc/infura/base-sepolia`, + fallbackRpcUrl: `${BASE_URL}/api/rpc/alchemy/base-sepolia`, + + color: "#627EEA", + isTestnet: true, + subdomain: "base-sepolia.drips.network", + gqlName: SupportedChain.BaseSepolia, + autoUnwrapPairs: [], + displayNetworkPicker: false, + applyGasBuffers: true, + explorer: { + name: "Etherscan", + linkTemplate: etherscanLinkTemplate, + }, + contracts: { + ADDRESS_DRIVER: "0x004310a6d47893Dd6e443cbE471c24aDA1e6c619", + DRIPS: "0xeebCd570e50fa31bcf6eF10f989429C87C3A6981", + CALLER: "0x5C7c5AA20b15e13229771CB7De36Fe1F54238372", + REPO_DRIVER: "0x54372850Db72915Fd9C5EC745683EB607b4a8642", + NFT_DRIVER: "0xDafd9Ab96E62941808caa115D184D30A200FA777", + NATIVE_TOKEN_UNWRAPPER: undefined, + }, + settlement: { + nextSettlementDate: nextMainnetSettlementDate, + explainerText: + "Funds from projects, streams and Drip Lists settle and become collectable on the last Thursday of each month.", + }, + alternativeChainMode: true, + }, + [314]: { + chainId: 314, + name: "filecoin", + label: "Filecoin", + token: "FIL", + id: "0x13a", + rpcUrl: `${BASE_URL}/api/rpc/glif/filecoin-mainnet`, + + color: "#627EEA", + isTestnet: false, + subdomain: "filecoin.drips.network", + gqlName: SupportedChain.Filecoin, + autoUnwrapPairs: [ + { + name: "Filecoin", + nativeSymbol: "FIL", + wrappedSymbol: "WFIL", + }, + ], + displayNetworkPicker: true, + applyGasBuffers: false, + explorer: { + name: "Blockscout", + linkTemplate: (txHash: string) => + `https://filecoin.blockscout.com/tx/${txHash}`, + }, + contracts: { + ADDRESS_DRIVER: "0x04693D13826a37dDdF973Be4275546Ad978cb9EE", + DRIPS: "0xd320F59F109c618b19707ea5C5F068020eA333B3", + CALLER: "0xd6Ab8e72dE3742d45AdF108fAa112Cd232718828", + REPO_DRIVER: "0xe75f56B26857cAe06b455Bfc9481593Ae0FB4257", + NFT_DRIVER: "0x2F23217A87cAf04ae586eed7a3d689f6C48498dB", + NATIVE_TOKEN_UNWRAPPER: "0x64e0d60C70e9778C2E649FfBc90259C86a6Bf396", + }, + settlement: { + nextSettlementDate: nextFilecoinSettlementDate, + explainerText: + "Funds from projects, streams and Drip Lists settle and become collectable daily.", + }, + alternativeChainMode: true, + }, +}; + +export function isSupportedChainId(chainId: number): chainId is ChainId { + return SUPPORTED_CHAIN_IDS.includes(chainId as ChainId); +} + +const PUBLIC_NETWORK = getOptionalEnvVar("PUBLIC_NETWORK") ?? "11155111"; +const configuredChainId = Number(PUBLIC_NETWORK); +assert( + isSupportedChainId(configuredChainId), + "Missing or invalid PUBLIC_NETWORK env variable. See DEVELOPMENT.md for more information.", +); + +export function isConfiguredChainId(chainId: number): boolean { + return chainId === configuredChainId; +} + +export function getNetwork(chainId: number): Network { + assert(isSupportedChainId(chainId), "Unsupported chain id"); + + return NETWORK_CONFIG[chainId]; +} + +export default getNetwork(configuredChainId); diff --git a/packages/sdk/src/drips/common/wallet/provider.ts b/packages/sdk/src/drips/common/wallet/provider.ts new file mode 100644 index 0000000..1008bb0 --- /dev/null +++ b/packages/sdk/src/drips/common/wallet/provider.ts @@ -0,0 +1,60 @@ +import { JsonRpcProvider, JsonRpcSigner } from "ethers"; + +const NETWORK = { + chainId: 11155111, + name: "sepolia", +}; + +export function getOptionalEnvVar(name: string): string | undefined { + const env = process.env; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return name in env ? (env as any)[name] : undefined; +} + +class MockProvider extends JsonRpcProvider { + address = "0x433220a86126eFe2b8C98a723E73eBAd2D0CbaDc"; + + setAddress(to: string) { + this.address = to; + } + + async request(request: { method: string; params?: unknown[] }) { + if (request.method === "eth_requestAccounts") { + return [this.address]; + } + // eslint-disable-next-line no-console + console.log("UNIMPLEMENTED METHOD", request); + } + + override async getSigner(): Promise { + const tempProvider = new JsonRpcProvider( + `http://${ + getOptionalEnvVar("PUBLIC_TESTNET_MOCK_PROVIDER_HOST") ?? "127.0.0.1" + }:8545`, + NETWORK, + { + staticNetwork: true, + }, + ); + + return await tempProvider.getSigner(this.address); + } + + isWeb3 = true; +} + +export default (address: string) => { + const provider = new MockProvider( + `http://${ + getOptionalEnvVar("PUBLIC_TESTNET_MOCK_PROVIDER_HOST") ?? "127.0.0.1" + }:8545`, + NETWORK, + { + staticNetwork: true, + }, + ); + provider.setAddress(address); + + return provider; +}; diff --git a/packages/sdk/src/drips/createDripList.ts b/packages/sdk/src/drips/createDripList.ts new file mode 100644 index 0000000..9f0a7a1 --- /dev/null +++ b/packages/sdk/src/drips/createDripList.ts @@ -0,0 +1,9 @@ +import assert from "node:assert"; +import type { State } from "&/drips/common/types.js"; + +export const createContinuousSupportDripList = async (state: State) => { + assert( + state.selectedSupportOption === 1, + "Only `continuous` mode is supported", + ); +}; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts new file mode 100644 index 0000000..a8141d3 --- /dev/null +++ b/packages/sdk/src/index.ts @@ -0,0 +1 @@ +console.log("Hello, world!"); diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json new file mode 100644 index 0000000..179033a --- /dev/null +++ b/packages/sdk/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "noImplicitOverride": true, + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "baseUrl": ".", + "paths": { + "&/*": ["src/*"] + } + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules"] +} From 227a68a65e52bbc8e05824a2fac6fbb83fd481f1 Mon Sep 17 00:00:00 2001 From: Javier R Date: Sat, 19 Oct 2024 17:46:30 +0200 Subject: [PATCH 2/4] add: `sdk` package to `turbo` --- package.json | 1 + pnpm-workspace.yaml | 1 + turbo.json | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/package.json b/package.json index 0bd16ce..247ce96 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "build": "turbo run build --concurrency=100%", "build:docs": "turbo run build --filter=@kariba/docs", "dev:docs": "turbo run dev --filter=@kariba/docs --parallel", + "build:sdk": "turbo run build --filter=@kariba/sdk", "format:staged": "lint-staged", "lint": "turbo run lint --concurrency=100%", "test": "turbo run test --concurrency=1", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2dbea27..c6a2d0a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,3 @@ packages: - "apps/docs" + - "packages/sdk" diff --git a/turbo.json b/turbo.json index e2f4361..d7117fe 100644 --- a/turbo.json +++ b/turbo.json @@ -36,6 +36,11 @@ "outputs": ["out/**", ".next/**"], "env": ["PLASMIC_PROJECT_ID", "PLASMIC_PROJECT_API_TOKEN"], "cache": false + }, + "@kariba/sdk#build": { + "dependsOn": ["^build"], + "outputs": ["dist/**"], + "cache": false } } } From b5dea9e2c944f42cc690721f33ba6c592b42008c Mon Sep 17 00:00:00 2001 From: Javier R Date: Sat, 19 Oct 2024 17:49:57 +0200 Subject: [PATCH 3/4] fix: update `lockfile` --- pnpm-lock.yaml | 566 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 548 insertions(+), 18 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c54670..2f254b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,8 +104,54 @@ importers: specifier: ~5.6.3 version: 5.6.3 + packages/sdk: + dependencies: + '@apollo/client': + specifier: ^3.11.8 + version: 3.11.8(@types/react@18.3.11)(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@efstajas/versioned-parser': + specifier: ^0.1.4 + version: 0.1.4 + '@octokit/rest': + specifier: ^21.0.2 + version: 21.0.2 + abitype: + specifier: ^1.0.6 + version: 1.0.6(typescript@5.6.3)(zod@3.23.8) + add: + specifier: ^2.0.6 + version: 2.0.6 + ethers: + specifier: ^6.13.4 + version: 6.13.4 + graphql: + specifier: ^16.9.0 + version: 16.9.0 + graphql-request: + specifier: ^7.1.0 + version: 7.1.0(graphql@16.9.0) + lodash: + specifier: ^4.17.21 + version: 4.17.21 + zod: + specifier: ^3.23.8 + version: 3.23.8 + devDependencies: + '@types/node': + specifier: ^22.7.6 + version: 22.7.7 + prettier: + specifier: ^3.3.3 + version: 3.3.3 + typescript: + specifier: ~5.6.3 + version: 5.6.3 + packages: + '@adraffy/ens-normalize@1.10.1': + resolution: {integrity: sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==} + '@algolia/autocomplete-core@1.9.3': resolution: {integrity: sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==} @@ -178,6 +224,24 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@apollo/client@3.11.8': + resolution: {integrity: sha512-CgG1wbtMjsV2pRGe/eYITmV5B8lXUCYljB2gB/6jWTFQcrvirUVvKg7qtFdjYkQSFbIffU1IDyxgeaN81eTjbA==} + peerDependencies: + graphql: ^15.0.0 || ^16.0.0 + graphql-ws: ^5.5.5 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + '@babel/code-frame@7.25.7': resolution: {integrity: sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==} engines: {node: '>=6.9.0'} @@ -928,6 +992,9 @@ packages: '@docusaurus/types': optional: true + '@efstajas/versioned-parser@0.1.4': + resolution: {integrity: sha512-R/MUEOeMGvegThqacHCasp03RtE66szRqt9d6Qa+LI2lNweyV1SyAk66JlgRppoJHXwmcIDJ5hAUEws0g/Xx2Q==} + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -986,6 +1053,11 @@ packages: '@formatjs/intl-localematcher@0.5.5': resolution: {integrity: sha512-t5tOGMgZ/i5+ALl2/offNqAQq/lfUnKLEw0mXQI4N4bqpedhrSE+fyKLpwnd22sK0dif6AV+ufQcTsKShB9J1g==} + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + '@hapi/hoek@9.3.0': resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} @@ -1064,6 +1136,12 @@ packages: '@types/react': '>=16' react: '>=16' + '@molt/command@0.9.0': + resolution: {integrity: sha512-1JI8dAlpqlZoXyKWVQggX7geFNPxBpocHIXQCsnxDjKy+3WX4SGyZVJXuLlqRRrX7FmQCuuMAfx642ovXmPA9g==} + + '@molt/types@0.2.0': + resolution: {integrity: sha512-p6ChnEZDGjg9PYPec9BK6Yp5/DdSrYQvXTBAtgrnqX6N36cZy37ql1c8Tc5LclfIYBNG7EZp8NBcRTYJwyi84g==} + '@ndhoule/each@2.0.1': resolution: {integrity: sha512-wHuJw6x+rF6Q9Skgra++KccjBozCr9ymtna0FhxmV/8xT/hZ2ExGYR8SV8prg8x4AH/7mzDYErNGIVHuzHeybw==} @@ -1073,6 +1151,13 @@ packages: '@ndhoule/map@2.0.1': resolution: {integrity: sha512-WOEf2An9mL4DVY6NHgaRmFC82pZGrmzW4I0hpPPdczDP4Gp5+Q1Nny77x3w0qzENA8+cbgd9+Lx2ClSTLvkB0g==} + '@noble/curves@1.2.0': + resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} + + '@noble/hashes@1.3.2': + resolution: {integrity: sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==} + engines: {node: '>= 16'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1085,6 +1170,58 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@octokit/auth-token@5.1.1': + resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.2': + resolution: {integrity: sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.1': + resolution: {integrity: sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.1.1': + resolution: {integrity: sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@22.2.0': + resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} + + '@octokit/plugin-paginate-rest@11.3.5': + resolution: {integrity: sha512-cgwIRtKrpwhLoBi0CUNuY83DPGRMaWVjqVI/bGKsLJ4PzyWZNaEmhHroI2xlrVXkk6nFv0IsZpOp+ZWSWUS2AQ==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@13.2.6': + resolution: {integrity: sha512-wMsdyHMjSfKjGINkdGKki06VEkgdEldIGstIEyGX0wbYHGByOwN/KiM+hAAlUwAtPkP3gvXtVQA9L3ITdV2tVw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@6.1.5': + resolution: {integrity: sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==} + engines: {node: '>= 18'} + + '@octokit/request@9.1.3': + resolution: {integrity: sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==} + engines: {node: '>= 18'} + + '@octokit/rest@21.0.2': + resolution: {integrity: sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==} + engines: {node: '>= 18'} + + '@octokit/types@13.6.1': + resolution: {integrity: sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==} + '@plasmicapp/auth-api@0.0.17': resolution: {integrity: sha512-mdcQgmYTxzFrmOSEV3FkHfX22KqzWAQFRaxYEC1DZqm5Jc/vvYVLO+TwkW1Y7O7TV9tUDEMDuy3JAFz9z5tsbw==} engines: {node: '>=10'} @@ -1568,6 +1705,9 @@ packages: '@types/node@22.7.5': resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/node@22.7.7': + resolution: {integrity: sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==} + '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -1733,12 +1873,43 @@ packages: '@webassemblyjs/wast-printer@1.12.1': resolution: {integrity: sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==} + '@wry/caches@1.0.1': + resolution: {integrity: sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==} + engines: {node: '>=8'} + + '@wry/context@0.7.4': + resolution: {integrity: sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==} + engines: {node: '>=8'} + + '@wry/equality@0.5.7': + resolution: {integrity: sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==} + engines: {node: '>=8'} + + '@wry/trie@0.4.3': + resolution: {integrity: sha512-I6bHwH0fSf6RqQcnnXLJKhkSXG45MFral3GxPaY4uAl0LYDZM+YDVDAiU9bYwjTuysy1S0IeecWtmq1SZA3M1w==} + engines: {node: '>=8'} + + '@wry/trie@0.5.0': + resolution: {integrity: sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==} + engines: {node: '>=8'} + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} '@xtuc/long@4.2.2': resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + abitype@1.0.6: + resolution: {integrity: sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3 >=3.22.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -1762,10 +1933,16 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + add@2.0.6: + resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==} + address@1.2.2: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} + aes-js@4.0.0-beta.5: + resolution: {integrity: sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==} + aggregate-error@3.1.0: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} @@ -1798,6 +1975,9 @@ packages: ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + alge@0.8.1: + resolution: {integrity: sha512-kiV9nTt+XIauAXsowVygDxMZLplZxDWt0W8plE/nB32/V2ziM/P/TxDbSVK7FYIUt2Xo16h3/htDh199LNPCKQ==} + algoliasearch-helper@3.22.5: resolution: {integrity: sha512-lWvhdnc+aKOKx8jyA3bsdEgHzm/sglC4cYdMG4xSQyRiPLJVJtH/IVYZG3Hp6PkTEhQqhyVYkeP9z2IlcHJsWw==} peerDependencies: @@ -1943,6 +2123,9 @@ packages: batch@0.6.1: resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==} + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + big.js@5.2.2: resolution: {integrity: sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==} @@ -2739,6 +2922,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + ethers@6.13.4: + resolution: {integrity: sha512-21YtnZVg4/zKkCQPjrDj38B1r4nQvTZLopUGMLQ1ePU2zV/joCfDC3t3iKQjWRzjjjbzR+mdAIoikeBRNkdllA==} + engines: {node: '>=14.0.0'} + eval@0.1.8: resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} engines: {node: '>= 0.8'} @@ -3033,6 +3220,32 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + graphql-request@7.1.0: + resolution: {integrity: sha512-Ouu/lYVFhARS1aXeZoVJWnGT6grFJXTLwXJuK4mUGGRo0EUk1JkyYp43mdGmRgUVezpRm6V5Sq3t8jBDQcajng==} + hasBin: true + peerDependencies: + '@dprint/formatter': ^0.3.0 + '@dprint/typescript': ^0.91.1 + dprint: ^0.46.2 + graphql: 14 - 16 + peerDependenciesMeta: + '@dprint/formatter': + optional: true + '@dprint/typescript': + optional: true + dprint: + optional: true + + graphql-tag@2.12.6: + resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} + engines: {node: '>=10'} + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + + graphql@16.9.0: + resolution: {integrity: sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gray-matter@4.0.3: resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} engines: {node: '>=6.0'} @@ -3652,15 +3865,24 @@ packages: resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lodash.camelcase@4.3.0: + resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} + lodash.debounce@4.0.8: resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + lodash.ismatch@4.4.0: + resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} @@ -4102,6 +4324,9 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true + optimism@0.18.0: + resolution: {integrity: sha512-tGn8+REwLRNFnb9WmcY5IfpOqeX2kpaYJ1s6Ae3mn12AeydLkR3j+jSCmVQFoXqU8D41PAJ1RG1rCRNWmNZVmQ==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4676,6 +4901,10 @@ packages: reading-time@1.5.0: resolution: {integrity: sha512-onYyVhBNr4CmAxFsKS7bz+uTLRakypIe4R+5A824vBSkQy/hB3fZepoVEf8OVAxzLvK+H/jm9TzpI3ETSm64Kg==} + readline-sync@1.4.10: + resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==} + engines: {node: '>= 0.8.0'} + rechoir@0.6.2: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} @@ -4724,6 +4953,17 @@ packages: resolution: {integrity: sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==} hasBin: true + rehackt@0.1.0: + resolution: {integrity: sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==} + peerDependencies: + '@types/react': '*' + react: '*' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + rehype-raw@7.0.0: resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} @@ -4756,6 +4996,9 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remeda@1.61.0: + resolution: {integrity: sha512-caKfSz9rDeSKBQQnlJnVW3mbVdFgxgGWQKq1XlFokqjf+hQD5gxutLGTTY2A/x24UxVyJe9gH5fAkFI63ULw4A==} + renderkid@3.0.0: resolution: {integrity: sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==} @@ -4791,6 +5034,10 @@ packages: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true + response-iterator@0.2.6: + resolution: {integrity: sha512-pVzEEzrsg23Sh053rmDUvLSkGXluZio0qu8VT6ukrYuvtjVfCbDZH9d6PGXb8HZfzdNZt8feXv/jvUzlhRgLnw==} + engines: {node: '>=0.8'} + responselike@3.0.0: resolution: {integrity: sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==} engines: {node: '>=14.16'} @@ -5043,6 +5290,10 @@ packages: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} + string-length@6.0.0: + resolution: {integrity: sha512-1U361pxZHEQ+FeSjzqRpV+cu2vTzYeWeafXFLykiFlv4Vc0n3njgU8HrMbyik5uwm77naWMuVG8fhEF+Ovb1Kg==} + engines: {node: '>=16'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5155,6 +5406,10 @@ packages: peerDependencies: react: ^16.11.0 || ^17.0.0 || ^18.0.0 + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + tapable@1.1.3: resolution: {integrity: sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==} engines: {node: '>=6'} @@ -5228,6 +5483,16 @@ packages: peerDependencies: typescript: '>=4.2.0' + ts-invariant@0.10.3: + resolution: {integrity: sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==} + engines: {node: '>=8'} + + ts-toolbelt@9.6.0: + resolution: {integrity: sha512-nsZd8ZeNUzukXPlJmTBwUAuABDe/9qtVDelJeT/qW0ow3ZS3BsQJtNkan1802aM9Uf68/Y8ljw86Hu0h5IUW3w==} + + tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + tslib@2.8.0: resolution: {integrity: sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==} @@ -5277,6 +5542,10 @@ packages: resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} engines: {node: '>=12.20'} + type-fest@4.26.1: + resolution: {integrity: sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==} + engines: {node: '>=16'} + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -5359,6 +5628,9 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -5575,6 +5847,18 @@ packages: utf-8-validate: optional: true + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + 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 + ws@8.18.0: resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} engines: {node: '>=10.0.0'} @@ -5627,11 +5911,22 @@ packages: resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} engines: {node: '>=12.20'} + zen-observable-ts@1.2.5: + resolution: {integrity: sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg==} + + zen-observable@0.8.15: + resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} snapshots: + '@adraffy/ens-normalize@1.10.1': {} + '@algolia/autocomplete-core@1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.17.2)': dependencies: '@algolia/autocomplete-plugin-algolia-insights': 1.9.3(@algolia/client-search@4.24.0)(algoliasearch@4.24.0)(search-insights@2.17.2) @@ -5743,6 +6038,29 @@ snapshots: '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 + '@apollo/client@3.11.8(@types/react@18.3.11)(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0) + '@wry/caches': 1.0.1 + '@wry/equality': 0.5.7 + '@wry/trie': 0.5.0 + graphql: 16.9.0 + graphql-tag: 2.12.6(graphql@16.9.0) + hoist-non-react-statics: 3.3.2 + optimism: 0.18.0 + prop-types: 15.8.1 + rehackt: 0.1.0(@types/react@18.3.11)(react@18.3.1) + response-iterator: 0.2.6 + symbol-observable: 4.0.0 + ts-invariant: 0.10.3 + tslib: 2.8.0 + zen-observable-ts: 1.2.5 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + '@babel/code-frame@7.25.7': dependencies: '@babel/highlight': 7.25.7 @@ -7181,6 +7499,8 @@ snapshots: - uglify-js - webpack-cli + '@efstajas/versioned-parser@0.1.4': {} + '@eslint-community/eslint-utils@4.4.0(eslint@9.12.0(jiti@1.21.6))': dependencies: eslint: 9.12.0(jiti@1.21.6) @@ -7249,6 +7569,10 @@ snapshots: dependencies: tslib: 2.8.0 + '@graphql-typed-document-node/core@3.2.0(graphql@16.9.0)': + dependencies: + graphql: 16.9.0 + '@hapi/hoek@9.3.0': {} '@hapi/topo@5.1.0': @@ -7292,7 +7616,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -7358,6 +7682,24 @@ snapshots: '@types/react': 18.3.11 react: 18.3.1 + '@molt/command@0.9.0': + dependencies: + '@molt/types': 0.2.0 + alge: 0.8.1 + chalk: 5.3.0 + lodash.camelcase: 4.3.0 + lodash.snakecase: 4.1.1 + readline-sync: 1.4.10 + string-length: 6.0.0 + strip-ansi: 7.1.0 + ts-toolbelt: 9.6.0 + type-fest: 4.26.1 + zod: 3.23.8 + + '@molt/types@0.2.0': + dependencies: + ts-toolbelt: 9.6.0 + '@ndhoule/each@2.0.1': dependencies: '@ndhoule/keys': 2.0.0 @@ -7368,6 +7710,12 @@ snapshots: dependencies: '@ndhoule/each': 2.0.1 + '@noble/curves@1.2.0': + dependencies: + '@noble/hashes': 1.3.2 + + '@noble/hashes@1.3.2': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -7380,6 +7728,67 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@octokit/auth-token@5.1.1': {} + + '@octokit/core@6.1.2': + dependencies: + '@octokit/auth-token': 5.1.1 + '@octokit/graphql': 8.1.1 + '@octokit/request': 9.1.3 + '@octokit/request-error': 6.1.5 + '@octokit/types': 13.6.1 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.2 + + '@octokit/endpoint@10.1.1': + dependencies: + '@octokit/types': 13.6.1 + universal-user-agent: 7.0.2 + + '@octokit/graphql@8.1.1': + dependencies: + '@octokit/request': 9.1.3 + '@octokit/types': 13.6.1 + universal-user-agent: 7.0.2 + + '@octokit/openapi-types@22.2.0': {} + + '@octokit/plugin-paginate-rest@11.3.5(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/types': 13.6.1 + + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + + '@octokit/plugin-rest-endpoint-methods@13.2.6(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/types': 13.6.1 + + '@octokit/request-error@6.1.5': + dependencies: + '@octokit/types': 13.6.1 + + '@octokit/request@9.1.3': + dependencies: + '@octokit/endpoint': 10.1.1 + '@octokit/request-error': 6.1.5 + '@octokit/types': 13.6.1 + universal-user-agent: 7.0.2 + + '@octokit/rest@21.0.2': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/plugin-paginate-rest': 11.3.5(@octokit/core@6.1.2) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.2) + '@octokit/plugin-rest-endpoint-methods': 13.2.6(@octokit/core@6.1.2) + + '@octokit/types@13.6.1': + dependencies: + '@octokit/openapi-types': 22.2.0 + '@plasmicapp/auth-api@0.0.17': dependencies: '@plasmicapp/isomorphic-unfetch': 1.0.3 @@ -7957,20 +8366,20 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/bonjour@3.5.13': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 5.0.0 - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/connect@3.4.38': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/debug@4.1.12': dependencies: @@ -7984,14 +8393,14 @@ snapshots: '@types/express-serve-static-core@4.19.6': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/qs': 6.9.16 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 '@types/express-serve-static-core@5.0.0': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/qs': 6.9.16 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -8019,7 +8428,7 @@ snapshots: '@types/http-proxy@1.17.15': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/istanbul-lib-coverage@2.0.6': {} @@ -8045,7 +8454,7 @@ snapshots: '@types/node-forge@1.3.11': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/node@17.0.45': {} @@ -8053,6 +8462,10 @@ snapshots: dependencies: undici-types: 6.19.8 + '@types/node@22.7.7': + dependencies: + undici-types: 6.19.8 + '@types/parse-json@4.0.2': {} '@types/prismjs@1.26.4': {} @@ -8089,12 +8502,12 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 17.0.45 + '@types/node': 22.7.7 '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/serve-index@1.9.4': dependencies: @@ -8103,12 +8516,12 @@ snapshots: '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/send': 0.17.4 '@types/sockjs@0.3.36': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/unist@2.0.11': {} @@ -8116,7 +8529,7 @@ snapshots: '@types/ws@8.5.12': dependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.7 '@types/yargs-parser@21.0.3': {} @@ -8283,10 +8696,35 @@ snapshots: '@webassemblyjs/ast': 1.12.1 '@xtuc/long': 4.2.2 + '@wry/caches@1.0.1': + dependencies: + tslib: 2.8.0 + + '@wry/context@0.7.4': + dependencies: + tslib: 2.8.0 + + '@wry/equality@0.5.7': + dependencies: + tslib: 2.8.0 + + '@wry/trie@0.4.3': + dependencies: + tslib: 2.8.0 + + '@wry/trie@0.5.0': + dependencies: + tslib: 2.8.0 + '@xtuc/ieee754@1.2.0': {} '@xtuc/long@4.2.2': {} + abitype@1.0.6(typescript@5.6.3)(zod@3.23.8): + optionalDependencies: + typescript: 5.6.3 + zod: 3.23.8 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -8306,8 +8744,12 @@ snapshots: acorn@8.13.0: {} + add@2.0.6: {} + address@1.2.2: {} + aes-js@4.0.0-beta.5: {} + aggregate-error@3.1.0: dependencies: clean-stack: 2.2.0 @@ -8345,6 +8787,13 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + alge@0.8.1: + dependencies: + lodash.ismatch: 4.4.0 + remeda: 1.61.0 + ts-toolbelt: 9.6.0 + zod: 3.23.8 + algoliasearch-helper@3.22.5(algoliasearch@4.24.0): dependencies: '@algolia/events': 4.0.1 @@ -8526,6 +8975,8 @@ snapshots: batch@0.6.1: {} + before-after-hook@3.0.2: {} + big.js@5.2.2: {} binary-extensions@2.3.0: {} @@ -9454,9 +9905,22 @@ snapshots: etag@1.8.1: {} - eval@0.1.8: + ethers@6.13.4: dependencies: + '@adraffy/ens-normalize': 1.10.1 + '@noble/curves': 1.2.0 + '@noble/hashes': 1.3.2 '@types/node': 22.7.5 + aes-js: 4.0.0-beta.5 + tslib: 2.7.0 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + eval@0.1.8: + dependencies: + '@types/node': 22.7.7 require-like: 0.1.2 eventemitter3@4.0.7: {} @@ -9810,6 +10274,20 @@ snapshots: graphemer@1.4.0: {} + graphql-request@7.1.0(graphql@16.9.0): + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0) + '@molt/command': 0.9.0 + graphql: 16.9.0 + zod: 3.23.8 + + graphql-tag@2.12.6(graphql@16.9.0): + dependencies: + graphql: 16.9.0 + tslib: 2.8.0 + + graphql@16.9.0: {} + gray-matter@4.0.3: dependencies: js-yaml: 3.14.1 @@ -10325,7 +10803,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.7.5 + '@types/node': 22.7.7 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -10333,13 +10811,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.7 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 22.7.5 + '@types/node': 22.7.7 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -10469,12 +10947,18 @@ snapshots: dependencies: p-locate: 6.0.0 + lodash.camelcase@4.3.0: {} + lodash.debounce@4.0.8: {} + lodash.ismatch@4.4.0: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.snakecase@4.1.1: {} + lodash.uniq@4.5.0: {} lodash@4.17.21: {} @@ -11181,6 +11665,13 @@ snapshots: opener@1.5.2: {} + optimism@0.18.0: + dependencies: + '@wry/caches': 1.0.1 + '@wry/context': 0.7.4 + '@wry/trie': 0.4.3 + tslib: 2.8.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -11771,6 +12262,8 @@ snapshots: reading-time@1.5.0: {} + readline-sync@1.4.10: {} + rechoir@0.6.2: dependencies: resolve: 1.22.8 @@ -11831,6 +12324,11 @@ snapshots: dependencies: jsesc: 3.0.2 + rehackt@0.1.0(@types/react@18.3.11)(react@18.3.1): + optionalDependencies: + '@types/react': 18.3.11 + react: 18.3.1 + rehype-raw@7.0.0: dependencies: '@types/hast': 3.0.4 @@ -11906,6 +12404,8 @@ snapshots: mdast-util-to-markdown: 2.1.0 unified: 11.0.5 + remeda@1.61.0: {} + renderkid@3.0.0: dependencies: css-select: 4.3.0 @@ -11940,6 +12440,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + response-iterator@0.2.6: {} + responselike@3.0.0: dependencies: lowercase-keys: 3.0.0 @@ -12234,6 +12736,10 @@ snapshots: string-argv@0.3.2: {} + string-length@6.0.0: + dependencies: + strip-ansi: 7.1.0 + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -12372,6 +12878,8 @@ snapshots: dependencies: react: 18.3.1 + symbol-observable@4.0.0: {} + tapable@1.1.3: {} tapable@2.2.1: {} @@ -12423,6 +12931,14 @@ snapshots: dependencies: typescript: 5.6.3 + ts-invariant@0.10.3: + dependencies: + tslib: 2.8.0 + + ts-toolbelt@9.6.0: {} + + tslib@2.7.0: {} + tslib@2.8.0: {} turbo-darwin-64@2.1.3: @@ -12460,6 +12976,8 @@ snapshots: type-fest@2.19.0: {} + type-fest@4.26.1: {} + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -12568,6 +13086,8 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universal-user-agent@7.0.2: {} + universalify@2.0.1: {} unpipe@1.0.0: {} @@ -12867,6 +13387,8 @@ snapshots: ws@7.5.10: {} + ws@8.17.1: {} + ws@8.18.0: {} xdg-basedir@5.1.0: {} @@ -12899,4 +13421,12 @@ snapshots: yocto-queue@1.1.1: {} + zen-observable-ts@1.2.5: + dependencies: + zen-observable: 0.8.15 + + zen-observable@0.8.15: {} + + zod@3.23.8: {} + zwitch@2.0.4: {} From 196629e960133589bce85526a439fc9afd70f5a9 Mon Sep 17 00:00:00 2001 From: Javier R Date: Sat, 19 Oct 2024 17:54:09 +0200 Subject: [PATCH 4/4] fix: add missing `function` --- packages/sdk/src/drips/common/graphql/dripQL.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/sdk/src/drips/common/graphql/dripQL.ts b/packages/sdk/src/drips/common/graphql/dripQL.ts index 5b97c98..4fd7c44 100644 --- a/packages/sdk/src/drips/common/graphql/dripQL.ts +++ b/packages/sdk/src/drips/common/graphql/dripQL.ts @@ -1,4 +1,3 @@ -import uniqBy from "lodash/uniqBy"; import { parse } from "graphql"; import { GraphQLClient, @@ -11,6 +10,10 @@ import { getOptionalEnvVar } from "&/drips/common/wallet/provider.js"; const envBaseUrl = getOptionalEnvVar("PUBLIC_BASE_URL"); const BASE_URL = envBaseUrl ?? "https://localhost:5173"; +function uniqBy(arr: T[], key: string): T[] { + return arr; +} + export default async function query< TResponse, TVariables extends Variables = Variables, @@ -30,7 +33,7 @@ export default async function query< return await client.request( { ...queryWithTypenames, - definitions: uniqBy(queryWithTypenames.definitions, "name.value"), + definitions: uniqBy([...queryWithTypenames.definitions], "name.value"), }, variables, );