From 8de17cc19b371dcaa21ac19f124d43f80ee9e9e3 Mon Sep 17 00:00:00 2001 From: Celo Composer Date: Tue, 16 Jun 2026 08:28:33 +0100 Subject: [PATCH 1/2] feat(sdk): add read-only functions, Celo param builders, and test validations --- packages/sdk/README.md | 117 ++++++++++++- packages/sdk/src/abi.json | 266 ++++++++++++++++++++++++++++- packages/sdk/src/index.ts | 340 +++++++++++++++++++++++++++++++++----- packages/sdk/test-cjs.js | 117 ++++++++++++- packages/sdk/test-esm.mjs | 117 ++++++++++++- 5 files changed, 909 insertions(+), 48 deletions(-) diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 66c97c9..41819dc 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -12,12 +12,16 @@ Install the SDK alongside its peer dependencies: npm install susuchain-sdk viem @stacks/network @stacks/transactions @stacks/connect ``` +> **Note:** `@stacks/connect` is an optional peer dependency — only required for browser wallet helpers. + ## Features - **Multi-Chain ABI & Addresses**: Instantly access Solidity ABIs and mainnet contract addresses for both Celo and Stacks. - **Stacks Browser Helpers**: Pre-packaged wallet triggers to easily call `create-circle`, `contribute`, and `trigger-payout` using the Leather wallet. - **Stacks Server-Side Builders**: Construct and sign Stacks transactions with a private key for backend or scripting environments. -- **Celo Transaction Param Builders**: Generate ready-to-use contract call parameter objects for viem wallet clients. +- **Stacks Read-Only Helpers**: Query on-chain Stacks state (`get-circle`, `get-circle-count`, `has-member-paid`, `is-member`) from Node.js. +- **Celo Transaction Param Builders**: Generate ready-to-use contract call parameter objects for viem wallet clients (both read and write operations). +- **TypeScript Interfaces**: Fully typed option interfaces for all builders. ## Usage @@ -61,7 +65,7 @@ async function getCircleDetails(circleId: number) { } ``` -### Celo Server-Side Transactions +### Celo Server-Side Transactions (Write) Build contract call parameters and pass them to a viem wallet client: @@ -91,6 +95,57 @@ const contributeParams = buildCeloContributeParams({ valueWei: 1000000000000000000n, }); const txHash = await wallet.writeContract(contributeParams); + +// Withdraw pending payouts +const withdrawParams = buildCeloWithdrawParams(); +await wallet.writeContract(withdrawParams); +``` + +### Celo Server-Side Read Operations + +Use param builders with a viem public client to query on-chain state: + +```typescript +import { createPublicClient, http } from 'viem'; +import { celo } from 'viem/chains'; +import { + buildCeloGetCircleParams, + buildCeloCircleCountParams, + buildCeloIsMemberParams, + buildCeloHasPaidParams, + buildCeloGetMemberCountParams, + buildCeloRoundBalanceParams, + buildCeloPendingWithdrawalsParams, +} from 'susuchain-sdk'; + +const client = createPublicClient({ chain: celo, transport: http() }); + +// Get total circle count +const count = await client.readContract(buildCeloCircleCountParams()); + +// Get circle details +const circle = await client.readContract(buildCeloGetCircleParams(0)); + +// Check membership +const isMember = await client.readContract( + buildCeloIsMemberParams({ circleId: 0, user: '0xAbc...' }) +); + +// Check payment status +const hasPaid = await client.readContract( + buildCeloHasPaidParams({ circleId: 0, round: 0, member: '0xAbc...' }) +); + +// Get member count +const memberCount = await client.readContract(buildCeloGetMemberCountParams(0)); + +// Get round balance +const balance = await client.readContract(buildCeloRoundBalanceParams(0)); + +// Check pending withdrawals +const pending = await client.readContract( + buildCeloPendingWithdrawalsParams('0xAbc...') +); ``` ### Stacks Integration (Leather Wallet) @@ -137,6 +192,33 @@ const result = await broadcastTx({ transaction: tx }); console.log('Broadcast result:', result); ``` +### Stacks Read-Only Queries + +Query on-chain Stacks state from server-side Node.js: + +```typescript +import { + readGetCircle, + readGetCircleCount, + readHasMemberPaid, + readIsMember, +} from 'susuchain-sdk'; + +const senderAddress = 'SP2T02XBVN9RAZ4360DSWF3JCG79B1QY2NR21RB0Q'; + +// Get circle details +const circle = await readGetCircle(0, senderAddress); + +// Get total circle count +const count = await readGetCircleCount(senderAddress); + +// Check if a member has paid +const hasPaid = await readHasMemberPaid(0, 0, 'SP3...', senderAddress); + +// Check membership +const isMember = await readIsMember(0, 'SP3...', senderAddress); +``` + ## Exported Constants ### Celo @@ -161,13 +243,40 @@ console.log('Broadcast result:', result); - `buildTriggerPayoutTx(opts)`: Build and sign a `trigger-payout` transaction. - `broadcastTx(opts)`: Broadcast a signed transaction to the Stacks network. -### Parameter Builders (Celo) +### Read-Only Helpers (Stacks) +- `readGetCircle(circleId, senderAddress)`: Query circle details. +- `readGetCircleCount(senderAddress)`: Query total circle count. +- `readHasMemberPaid(circleId, round, member, senderAddress)`: Check payment status. +- `readIsMember(circleId, user, senderAddress)`: Check membership. + +### Parameter Builders — Write (Celo) - `buildCeloCreateCircleParams(opts)`: Returns viem-compatible params for `createCircle`. - `buildCeloContributeParams(opts)`: Returns viem-compatible params for `contribute`. +- `buildCeloWithdrawParams()`: Returns viem-compatible params for `withdraw`. + +### Parameter Builders — Read (Celo) +- `buildCeloGetCircleParams(circleId)`: Returns params for `getCircle`. +- `buildCeloIsMemberParams(opts)`: Returns params for `isMember`. +- `buildCeloGetMemberCountParams(circleId)`: Returns params for `getMemberCount`. +- `buildCeloHasPaidParams(opts)`: Returns params for `hasPaid`. +- `buildCeloCircleCountParams()`: Returns params for `circleCount`. +- `buildCeloRoundBalanceParams(circleId)`: Returns params for `roundBalance`. +- `buildCeloPendingWithdrawalsParams(account)`: Returns params for `pendingWithdrawals`. + +## Exported TypeScript Interfaces + +- `CreateCircleTxOptions`: Options for Stacks `buildCreateCircleTx`. +- `ContributeTxOptions`: Options for Stacks `buildContributeTx`. +- `TriggerPayoutTxOptions`: Options for Stacks `buildTriggerPayoutTx`. +- `BroadcastTxOptions`: Options for `broadcastTx`. +- `CeloCreateCircleOptions`: Options for `buildCeloCreateCircleParams`. +- `CeloContributeOptions`: Options for `buildCeloContributeParams`. +- `CeloHasPaidOptions`: Options for `buildCeloHasPaidParams`. +- `CeloIsMemberOptions`: Options for `buildCeloIsMemberParams`. ## Tracking -This release satisfies clean ES Modules and CommonJS packaging guidelines in alignment with issue #27. +This release satisfies clean ES Modules and CommonJS packaging guidelines in alignment with issues #25 and #27. ## License diff --git a/packages/sdk/src/abi.json b/packages/sdk/src/abi.json index 157f23f..c0ecc9f 100644 --- a/packages/sdk/src/abi.json +++ b/packages/sdk/src/abi.json @@ -3,6 +3,21 @@ "contractName": "SusuChain", "sourceName": "contracts/SusuChain.sol", "abi": [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "EnforcedPause", + "type": "error" + }, + { + "inputs": [], + "name": "ExpectedPause", + "type": "error" + }, { "anonymous": false, "inputs": [ @@ -28,6 +43,25 @@ "name": "CircleCreated", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "minAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "maxAmount", + "type": "uint256" + } + ], + "name": "ContributionLimitsUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -59,6 +93,50 @@ "name": "ContributionMade", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Paused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "circleId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "round", + "type": "uint256" + } + ], + "name": "PayoutFailed", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -90,6 +168,38 @@ "name": "PayoutSent", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "Unpaused", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "recipient", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "Withdrawal", + "type": "event" + }, { "inputs": [], "name": "circleCount", @@ -142,6 +252,21 @@ "internalType": "bool", "name": "active", "type": "bool" + }, + { + "internalType": "uint256", + "name": "roundDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gracePeriod", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "penaltyFee", + "type": "uint256" } ], "stateMutability": "view", @@ -174,7 +299,17 @@ }, { "internalType": "uint256", - "name": "cycleDurationDays", + "name": "roundDurationDays", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gracePeriodDays", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "penaltyFee", "type": "uint256" }, { @@ -232,6 +367,21 @@ "internalType": "bool", "name": "active", "type": "bool" + }, + { + "internalType": "uint256", + "name": "roundDuration", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gracePeriod", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "penaltyFee", + "type": "uint256" } ], "stateMutability": "view", @@ -309,6 +459,84 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "maxContributionAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minContributionAmount", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "pendingWithdrawals", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -327,10 +555,42 @@ ], "stateMutability": "view", "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_minAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_maxAmount", + "type": "uint256" + } + ], + "name": "setContributionLimits", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "unpause", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "withdraw", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ], - "bytecode": "0x6080604052348015600f57600080fd5b506110978061001f6000396000f3fe6080604052600436106100865760003560e01c80637d9e10f5116100595780637d9e10f51461017f578063bc5e0f301461019f578063c1cbbca7146101d1578063c45d2852146101e6578063cf0b11171461020657600080fd5b80632cd19e621461008b5780632f33afb6146100cb5780635ce9284f1461011c57806364ccecde1461014c575b600080fd5b34801561009757600080fd5b506100b86100a6366004610b1e565b60026020526000908152604090205481565b6040519081526020015b60405180910390f35b3480156100d757600080fd5b5061010c6100e6366004610b53565b600160209081526000938452604080852082529284528284209052825290205460ff1681565b60405190151581526020016100c2565b34801561012857600080fd5b506100b8610137366004610b1e565b60009081526020819052604090206003015490565b34801561015857600080fd5b5061016c610167366004610b1e565b61021c565b6040516100c29796959493929190610bce565b34801561018b57600080fd5b5061010c61019a366004610c5e565b61036d565b3480156101ab57600080fd5b506101bf6101ba366004610b1e565b6103e2565b6040516100c296959493929190610c8a565b6101e46101df366004610b1e565b6104a2565b005b3480156101f257600080fd5b506101e4610201366004610d99565b61078c565b34801561021257600080fd5b506100b860035481565b606060008060606000806000806000808a81526020019081526020016000209050806000018160010154826002015483600301846004015485600501548660060160009054906101000a900460ff1686805461027790610e6e565b80601f01602080910402602001604051908101604052809291908181526020018280546102a390610e6e565b80156102f05780601f106102c5576101008083540402835291602001916102f0565b820191906000526020600020905b8154815290600101906020018083116102d357829003601f168201915b505050505096508380548060200260200160405190810160405280929190818152602001828054801561034c57602002820191906000526020600020905b81546001600160a01b0316815260019091019060200180831161032e575b50505050509350975097509750975097509750975050919395979092949650565b6000828152602081905260408120815b60038201548110156103d557836001600160a01b03168260030182815481106103a8576103a8610ea8565b6000918252602090912001546001600160a01b0316036103cd576001925050506103dc565b60010161037d565b5060009150505b92915050565b6000602081905290815260409020805481906103fd90610e6e565b80601f016020809104026020016040519081016040528092919081815260200182805461042990610e6e565b80156104765780601f1061044b57610100808354040283529160200191610476565b820191906000526020600020905b81548152906001019060200180831161045957829003601f168201915b505050600184015460028501546004860154600587015460069097015495969295919450925060ff1686565b6000818152602081905260409020600681015460ff166105005760405162461bcd60e51b8152602060048201526014602482015273436972636c65206973206e6f742061637469766560601b60448201526064015b60405180910390fd5b806001015434146105535760405162461bcd60e51b815260206004820152601960248201527f57726f6e6720636f6e747269627574696f6e20616d6f756e740000000000000060448201526064016104f7565b6000805b60038301548110156105ad57336001600160a01b031683600301828154811061058257610582610ea8565b6000918252602090912001546001600160a01b0316036105a557600191506105ad565b600101610557565b50806105ea5760405162461bcd60e51b815260206004820152600c60248201526b2737ba10309036b2b6b132b960a11b60448201526064016104f7565b600083815260016020908152604080832060048601548452825280832033845290915290205460ff16156106605760405162461bcd60e51b815260206004820152601760248201527f416c72656164792070616964207468697320726f756e6400000000000000000060448201526064016104f7565b60008381526001602081815260408084206004870154855282528084203385528252808420805460ff191690931790925585835260029052812080543492906106aa908490610ed4565b90915550506004820154604080513481526020810192909252339185917ff9b7589b7a8d939b9da5dae90770378fe5fd7802be5d66432e85bedbdbfbcb7a910160405180910390a3600160005b60038401548110156107765760008581526001602090815260408083206004880154845290915281206003860180549192918490811061073957610739610ea8565b60009182526020808320909101546001600160a01b0316835282019290925260400190205460ff1661076e5760009150610776565b6001016106f7565b5080156107865761078684610927565b50505050565b6002815110156107de5760405162461bcd60e51b815260206004820152601a60248201527f4d696e696d756d2032206d656d6265727320726571756972656400000000000060448201526064016104f7565b6000831161083d5760405162461bcd60e51b815260206004820152602660248201527f436f6e747269627574696f6e206d7573742062652067726561746572207468616044820152656e207a65726f60d01b60648201526084016104f7565b600380546000918261084e83610ee7565b90915550600081815260208190526040902090915061086d8682610f4f565b50600081815260208190526040902060010184905561088f836201518061100e565b600082815260208181526040909120600281019290925583516108b89260030191850190610aa4565b5060008181526020819052604080822060048101929092554260058301556006909101805460ff1916600117905551339082907f6f657d36deb3b1aa2e949c5df40eac9d79359a73c2402cefa7aee870f9ceafc090610918908990611025565b60405180910390a35050505050565b600081815260208190526040812060048101546003820180549293919261094e908461103f565b8154811061095e5761095e610ea8565b60009182526020808320909101548683526002909152604082208054908390556004860180546001600160a01b039093169450909261099c83610ee7565b909155505042600585015560038401546004850154036109c35760068401805460ff191690555b6000826001600160a01b03168260405160006040518083038185875af1925050503d8060008114610a10576040519150601f19603f3d011682016040523d82523d6000602084013e610a15565b606091505b5050905080610a565760405162461bcd60e51b815260206004820152600d60248201526c14185e5bdd5d0819985a5b1959609a1b60448201526064016104f7565b60408051838152602081018690526001600160a01b0385169188917fa66924c8a65f84761475175d1d8db947a42ff99992d9881649ceaec7045fe581910160405180910390a3505050505050565b828054828255906000526020600020908101928215610af9579160200282015b82811115610af957825182546001600160a01b0319166001600160a01b03909116178255602090920191600190910190610ac4565b50610b05929150610b09565b5090565b5b80821115610b055760008155600101610b0a565b600060208284031215610b3057600080fd5b5035919050565b80356001600160a01b0381168114610b4e57600080fd5b919050565b600080600060608486031215610b6857600080fd5b8335925060208401359150610b7f60408501610b37565b90509250925092565b6000815180845260005b81811015610bae57602081850181015186830182015201610b92565b506000602082860101526020601f19601f83011685010191505092915050565b60e081526000610be160e083018a610b88565b886020840152876040840152828103606084015280875180835260208301915060208901925060005b81811015610c315783516001600160a01b0316835260209384019390920191600101610c0a565b505080925050508460808301528360a0830152610c5260c083018415159052565b98975050505050505050565b60008060408385031215610c7157600080fd5b82359150610c8160208401610b37565b90509250929050565b60c081526000610c9d60c0830189610b88565b602083019790975250604081019490945260608401929092526080830152151560a090910152919050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff81118282101715610d0757610d07610cc8565b604052919050565b600082601f830112610d2057600080fd5b813567ffffffffffffffff811115610d3a57610d3a610cc8565b8060051b610d4a60208201610cde565b91825260208185018101929081019086841115610d6657600080fd5b6020860192505b83831015610d8f57610d7e83610b37565b825260209283019290910190610d6d565b9695505050505050565b60008060008060808587031215610daf57600080fd5b843567ffffffffffffffff811115610dc657600080fd5b8501601f81018713610dd757600080fd5b803567ffffffffffffffff811115610df157610df1610cc8565b610e04601f8201601f1916602001610cde565b818152886020838501011115610e1957600080fd5b8160208401602083013760006020928201830152955086013593505060408501359150606085013567ffffffffffffffff811115610e5657600080fd5b610e6287828801610d0f565b91505092959194509250565b600181811c90821680610e8257607f821691505b602082108103610ea257634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052603260045260246000fd5b634e487b7160e01b600052601160045260246000fd5b808201808211156103dc576103dc610ebe565b600060018201610ef957610ef9610ebe565b5060010190565b601f821115610f4a57806000526020600020601f840160051c81016020851015610f275750805b601f840160051c820191505b81811015610f475760008155600101610f33565b50505b505050565b815167ffffffffffffffff811115610f6957610f69610cc8565b610f7d81610f778454610e6e565b84610f00565b6020601f821160018114610fb15760008315610f995750848201515b600019600385901b1c1916600184901b178455610f47565b600084815260208120601f198516915b82811015610fe15787850151825560209485019460019092019101610fc1565b5084821015610fff5786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b80820281158282048414176103dc576103dc610ebe565b6020815260006110386020830184610b88565b9392505050565b60008261105c57634e487b7160e01b600052601260045260246000fd5b50069056fea264697066735822122085fc015faf93e710b19c708ec8505a717e697a3d02dbdae260bdc314fc47b22364736f6c634300081c0033", - "deployedBytecode": "0x6080604052600436106100865760003560e01c80637d9e10f5116100595780637d9e10f51461017f578063bc5e0f301461019f578063c1cbbca7146101d1578063c45d2852146101e6578063cf0b11171461020657600080fd5b80632cd19e621461008b5780632f33afb6146100cb5780635ce9284f1461011c57806364ccecde1461014c575b600080fd5b34801561009757600080fd5b506100b86100a6366004610b1e565b60026020526000908152604090205481565b6040519081526020015b60405180910390f35b3480156100d757600080fd5b5061010c6100e6366004610b53565b600160209081526000938452604080852082529284528284209052825290205460ff1681565b60405190151581526020016100c2565b34801561012857600080fd5b506100b8610137366004610b1e565b60009081526020819052604090206003015490565b34801561015857600080fd5b5061016c610167366004610b1e565b61021c565b6040516100c29796959493929190610bce565b34801561018b57600080fd5b5061010c61019a366004610c5e565b61036d565b3480156101ab57600080fd5b506101bf6101ba366004610b1e565b6103e2565b6040516100c296959493929190610c8a565b6101e46101df366004610b1e565b6104a2565b005b3480156101f257600080fd5b506101e4610201366004610d99565b61078c565b34801561021257600080fd5b506100b860035481565b606060008060606000806000806000808a81526020019081526020016000209050806000018160010154826002015483600301846004015485600501548660060160009054906101000a900460ff1686805461027790610e6e565b80601f01602080910402602001604051908101604052809291908181526020018280546102a390610e6e565b80156102f05780601f106102c5576101008083540402835291602001916102f0565b820191906000526020600020905b8154815290600101906020018083116102d357829003601f168201915b505050505096508380548060200260200160405190810160405280929190818152602001828054801561034c57602002820191906000526020600020905b81546001600160a01b0316815260019091019060200180831161032e575b50505050509350975097509750975097509750975050919395979092949650565b6000828152602081905260408120815b60038201548110156103d557836001600160a01b03168260030182815481106103a8576103a8610ea8565b6000918252602090912001546001600160a01b0316036103cd576001925050506103dc565b60010161037d565b5060009150505b92915050565b6000602081905290815260409020805481906103fd90610e6e565b80601f016020809104026020016040519081016040528092919081815260200182805461042990610e6e565b80156104765780601f1061044b57610100808354040283529160200191610476565b820191906000526020600020905b81548152906001019060200180831161045957829003601f168201915b505050600184015460028501546004860154600587015460069097015495969295919450925060ff1686565b6000818152602081905260409020600681015460ff166105005760405162461bcd60e51b8152602060048201526014602482015273436972636c65206973206e6f742061637469766560601b60448201526064015b60405180910390fd5b806001015434146105535760405162461bcd60e51b815260206004820152601960248201527f57726f6e6720636f6e747269627574696f6e20616d6f756e740000000000000060448201526064016104f7565b6000805b60038301548110156105ad57336001600160a01b031683600301828154811061058257610582610ea8565b6000918252602090912001546001600160a01b0316036105a557600191506105ad565b600101610557565b50806105ea5760405162461bcd60e51b815260206004820152600c60248201526b2737ba10309036b2b6b132b960a11b60448201526064016104f7565b600083815260016020908152604080832060048601548452825280832033845290915290205460ff16156106605760405162461bcd60e51b815260206004820152601760248201527f416c72656164792070616964207468697320726f756e6400000000000000000060448201526064016104f7565b60008381526001602081815260408084206004870154855282528084203385528252808420805460ff191690931790925585835260029052812080543492906106aa908490610ed4565b90915550506004820154604080513481526020810192909252339185917ff9b7589b7a8d939b9da5dae90770378fe5fd7802be5d66432e85bedbdbfbcb7a910160405180910390a3600160005b60038401548110156107765760008581526001602090815260408083206004880154845290915281206003860180549192918490811061073957610739610ea8565b60009182526020808320909101546001600160a01b0316835282019290925260400190205460ff1661076e5760009150610776565b6001016106f7565b5080156107865761078684610927565b50505050565b6002815110156107de5760405162461bcd60e51b815260206004820152601a60248201527f4d696e696d756d2032206d656d6265727320726571756972656400000000000060448201526064016104f7565b6000831161083d5760405162461bcd60e51b815260206004820152602660248201527f436f6e747269627574696f6e206d7573742062652067726561746572207468616044820152656e207a65726f60d01b60648201526084016104f7565b600380546000918261084e83610ee7565b90915550600081815260208190526040902090915061086d8682610f4f565b50600081815260208190526040902060010184905561088f836201518061100e565b600082815260208181526040909120600281019290925583516108b89260030191850190610aa4565b5060008181526020819052604080822060048101929092554260058301556006909101805460ff1916600117905551339082907f6f657d36deb3b1aa2e949c5df40eac9d79359a73c2402cefa7aee870f9ceafc090610918908990611025565b60405180910390a35050505050565b600081815260208190526040812060048101546003820180549293919261094e908461103f565b8154811061095e5761095e610ea8565b60009182526020808320909101548683526002909152604082208054908390556004860180546001600160a01b039093169450909261099c83610ee7565b909155505042600585015560038401546004850154036109c35760068401805460ff191690555b6000826001600160a01b03168260405160006040518083038185875af1925050503d8060008114610a10576040519150601f19603f3d011682016040523d82523d6000602084013e610a15565b606091505b5050905080610a565760405162461bcd60e51b815260206004820152600d60248201526c14185e5bdd5d0819985a5b1959609a1b60448201526064016104f7565b60408051838152602081018690526001600160a01b0385169188917fa66924c8a65f84761475175d1d8db947a42ff99992d9881649ceaec7045fe581910160405180910390a3505050505050565b828054828255906000526020600020908101928215610af9579160200282015b82811115610af957825182546001600160a01b0319166001600160a01b03909116178255602090920191600190910190610ac4565b50610b05929150610b09565b5090565b5b80821115610b055760008155600101610b0a565b600060208284031215610b3057600080fd5b5035919050565b80356001600160a01b0381168114610b4e57600080fd5b919050565b600080600060608486031215610b6857600080fd5b8335925060208401359150610b7f60408501610b37565b90509250925092565b6000815180845260005b81811015610bae57602081850181015186830182015201610b92565b506000602082860101526020601f19601f83011685010191505092915050565b60e081526000610be160e083018a610b88565b886020840152876040840152828103606084015280875180835260208301915060208901925060005b81811015610c315783516001600160a01b0316835260209384019390920191600101610c0a565b505080925050508460808301528360a0830152610c5260c083018415159052565b98975050505050505050565b60008060408385031215610c7157600080fd5b82359150610c8160208401610b37565b90509250929050565b60c081526000610c9d60c0830189610b88565b602083019790975250604081019490945260608401929092526080830152151560a090910152919050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff81118282101715610d0757610d07610cc8565b604052919050565b600082601f830112610d2057600080fd5b813567ffffffffffffffff811115610d3a57610d3a610cc8565b8060051b610d4a60208201610cde565b91825260208185018101929081019086841115610d6657600080fd5b6020860192505b83831015610d8f57610d7e83610b37565b825260209283019290910190610d6d565b9695505050505050565b60008060008060808587031215610daf57600080fd5b843567ffffffffffffffff811115610dc657600080fd5b8501601f81018713610dd757600080fd5b803567ffffffffffffffff811115610df157610df1610cc8565b610e04601f8201601f1916602001610cde565b818152886020838501011115610e1957600080fd5b8160208401602083013760006020928201830152955086013593505060408501359150606085013567ffffffffffffffff811115610e5657600080fd5b610e6287828801610d0f565b91505092959194509250565b600181811c90821680610e8257607f821691505b602082108103610ea257634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052603260045260246000fd5b634e487b7160e01b600052601160045260246000fd5b808201808211156103dc576103dc610ebe565b600060018201610ef957610ef9610ebe565b5060010190565b601f821115610f4a57806000526020600020601f840160051c81016020851015610f275750805b601f840160051c820191505b81811015610f475760008155600101610f33565b50505b505050565b815167ffffffffffffffff811115610f6957610f69610cc8565b610f7d81610f778454610e6e565b84610f00565b6020601f821160018114610fb15760008315610f995750848201515b600019600385901b1c1916600184901b178455610f47565b600084815260208120601f198516915b82811015610fe15787850151825560209485019460019092019101610fc1565b5084821015610fff5786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b80820281158282048414176103dc576103dc610ebe565b6020815260006110386020830184610b88565b9392505050565b60008261105c57634e487b7160e01b600052601260045260246000fd5b50069056fea264697066735822122085fc015faf93e710b19c708ec8505a717e697a3d02dbdae260bdc314fc47b22364736f6c634300081c0033", + "bytecode": "0x6080604052348015600f57600080fd5b50600680546001600160a01b0319163317905566038d7ea4c6800060075569021e19e0c9bab240000060085561185b8061004a6000396000f3fe6080604052600436106101095760003560e01c80637d9e10f511610095578063c1cbbca711610064578063c1cbbca714610317578063c66388221461032a578063c67e04d81461034a578063cf0b11171461036a578063f3f437031461038057600080fd5b80637d9e10f5146102755780638456cb59146102955780638da5cb5b146102aa578063bc5e0f30146102e257600080fd5b80633f4ba83a116100dc5780633f4ba83a146101cc57806345d5149f146101e15780635c975abb146101f75780635ce9284f1461020f57806364ccecde1461023f57600080fd5b806319cc1ec41461010e5780632cd19e62146101375780632f33afb6146101645780633ccfd60b146101b5575b600080fd5b34801561011a57600080fd5b5061012460085481565b6040519081526020015b60405180910390f35b34801561014357600080fd5b5061012461015236600461121f565b60036020526000908152604090205481565b34801561017057600080fd5b506101a561017f366004611254565b600260209081526000938452604080852082529284528284209052825290205460ff1681565b604051901515815260200161012e565b3480156101c157600080fd5b506101ca6103ad565b005b3480156101d857600080fd5b506101ca6104e3565b3480156101ed57600080fd5b5061012460075481565b34801561020357600080fd5b5060005460ff166101a5565b34801561021b57600080fd5b5061012461022a36600461121f565b60009081526001602052604090206003015490565b34801561024b57600080fd5b5061025f61025a36600461121f565b610517565b60405161012e9a999897969594939291906112cf565b34801561028157600080fd5b506101a5610290366004611378565b610685565b3480156102a157600080fd5b506101ca6106fa565b3480156102b657600080fd5b506006546102ca906001600160a01b031681565b6040516001600160a01b03909116815260200161012e565b3480156102ee57600080fd5b506103026102fd36600461121f565b61072c565b60405161012e999897969594939291906113a4565b6101ca61032536600461121f565b610801565b34801561033657600080fd5b506101ca6103453660046114c9565b610b48565b34801561035657600080fd5b506101ca6103653660046115b0565b610e4a565b34801561037657600080fd5b5061012460045481565b34801561038c57600080fd5b5061012461039b3660046115d2565b60056020526000908152604090205481565b6103b5610f0b565b336000908152600560205260409020548061040f5760405162461bcd60e51b8152602060048201526015602482015274139bc81c195b991a5b99c81dda5d1a191c985dd85b605a1b60448201526064015b60405180910390fd5b336000818152600560205260408082208290555190919083908381818185875af1925050503d8060008114610460576040519150601f19603f3d011682016040523d82523d6000602084013e610465565b606091505b50509050806104aa5760405162461bcd60e51b815260206004820152601160248201527015da5d1a191c985dd85b0819985a5b1959607a1b6044820152606401610406565b60405182815233907f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b659060200160405180910390a25050565b6006546001600160a01b0316331461050d5760405162461bcd60e51b8152600401610406906115f4565b610515610f2f565b565b606060008060606000806000806000806000600160008d81526020019081526020016000209050806000018160010154826002015483600301846004015485600501548660060160009054906101000a900460ff1687600701548860080154896009015489805461058790611639565b80601f01602080910402602001604051908101604052809291908181526020018280546105b390611639565b80156106005780601f106105d557610100808354040283529160200191610600565b820191906000526020600020905b8154815290600101906020018083116105e357829003601f168201915b505050505099508680548060200260200160405190810160405280929190818152602001828054801561065c57602002820191906000526020600020905b81546001600160a01b0316815260019091019060200180831161063e575b505050505096509a509a509a509a509a509a509a509a509a509a50509193959799509193959799565b6000828152600160205260408120815b60038201548110156106ed57836001600160a01b03168260030182815481106106c0576106c0611673565b6000918252602090912001546001600160a01b0316036106e5576001925050506106f4565b600101610695565b5060009150505b92915050565b6006546001600160a01b031633146107245760405162461bcd60e51b8152600401610406906115f4565b610515610f81565b60016020526000908152604090208054819061074790611639565b80601f016020809104026020016040519081016040528092919081815260200182805461077390611639565b80156107c05780601f10610795576101008083540402835291602001916107c0565b820191906000526020600020905b8154815290600101906020018083116107a357829003601f168201915b50505060018401546002850154600486015460058701546006880154600789015460088a01546009909a015498999598949750929550909360ff9091169289565b610809610f0b565b6000818152600160205260409020600681015460ff166108625760405162461bcd60e51b8152602060048201526014602482015273436972636c65206973206e6f742061637469766560601b6044820152606401610406565b3460000361086e575050565b600081600701548260050154610884919061169f565b90506000826008015482610898919061169f565b4211905060008360010154905081156108bd5760098401546108ba908261169f565b90505b80341461090c5760405162461bcd60e51b815260206004820152601960248201527f57726f6e6720636f6e747269627574696f6e20616d6f756e74000000000000006044820152606401610406565b6000805b600386015481101561096657336001600160a01b031686600301828154811061093b5761093b611673565b6000918252602090912001546001600160a01b03160361095e5760019150610966565b600101610910565b50806109a35760405162461bcd60e51b815260206004820152600c60248201526b2737ba10309036b2b6b132b960a11b6044820152606401610406565b600086815260026020908152604080832060048901548452825280832033845290915290205460ff1615610a195760405162461bcd60e51b815260206004820152601760248201527f416c72656164792070616964207468697320726f756e640000000000000000006044820152606401610406565b60008681526002602090815260408083206004890154845282528083203384528252808320805460ff19166001179055888352600390915281208054349290610a6390849061169f565b90915550506004850154604080513481526020810192909252339188917ff9b7589b7a8d939b9da5dae90770378fe5fd7802be5d66432e85bedbdbfbcb7a910160405180910390a3600160005b6003870154811015610b2f57600088815260026020908152604080832060048b01548452909152812060038901805491929184908110610af257610af2611673565b60009182526020808320909101546001600160a01b0316835282019290925260400190205460ff16610b275760009150610b2f565b600101610ab0565b508015610b3f57610b3f87610fbe565b50505050505050565b610b50610f0b565b600281511015610ba25760405162461bcd60e51b815260206004820152601a60248201527f4d696e696d756d2032206d656d626572732072657175697265640000000000006044820152606401610406565b60005b8151811015610c7a576000610bbb82600161169f565b90505b8251811015610c7157828181518110610bd957610bd9611673565b60200260200101516001600160a01b0316838381518110610bfc57610bfc611673565b60200260200101516001600160a01b031603610c695760405162461bcd60e51b815260206004820152602660248201527f4475706c6963617465206d656d62657220616464726573736573206e6f7420616044820152651b1b1bddd95960d21b6064820152608401610406565b600101610bbe565b50600101610ba5565b50600754851015610cc45760405162461bcd60e51b8152602060048201526014602482015273436f6e747269627574696f6e20746f6f206c6f7760601b6044820152606401610406565b600854851115610d0e5760405162461bcd60e51b8152602060048201526015602482015274086dedce8e4d2c4eae8d2dedc40e8dede40d0d2ced605b1b6044820152606401610406565b6004805460009182610d1f836116b2565b909155506000818152600160205260409020909150610d3e888261171a565b50600081815260016020819052604090912001869055610d6185620151806117d9565b600082815260016020908152604090912060028101929092558351610d8c92600301918501906111a5565b506000818152600160208190526040822060048101929092554260058301556006909101805460ff19169091179055610dc885620151806117d9565b600082815260016020526040902060070155610de784620151806117d9565b600082815260016020526040908190206008810192909255600990910184905551339082907f6f657d36deb3b1aa2e949c5df40eac9d79359a73c2402cefa7aee870f9ceafc090610e39908b906117f0565b60405180910390a350505050505050565b6006546001600160a01b03163314610e745760405162461bcd60e51b8152600401610406906115f4565b80821115610ec45760405162461bcd60e51b815260206004820152601e60248201527f4d696e206c696d6974206d757374206265203c3d206d6178206c696d697400006044820152606401610406565b6007829055600881905560408051838152602081018390527f33aa92491b46a02a93d5a2523a688c5bb3f0192474ba74d63d506220094099ff910160405180910390a15050565b60005460ff16156105155760405163d93c066560e01b815260040160405180910390fd5b610f37611182565b6000805460ff191690557f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b6040516001600160a01b03909116815260200160405180910390a1565b610f89610f0b565b6000805460ff191660011790557f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258610f643390565b6000818152600160205260408120600481015460038201805492939192610fe59084611803565b81548110610ff557610ff5611673565b60009182526020808320909101548683526003909152604082208054908390556004860180546001600160a01b0390931694509092611033836116b2565b9091555050426005850155600384015460048501540361105a5760068401805460ff191690555b6000826001600160a01b03168261c35090604051600060405180830381858888f193505050503d80600081146110ac576040519150601f19603f3d011682016040523d82523d6000602084013e6110b1565b606091505b5050905080156111065760408051838152602081018690526001600160a01b0385169188917fa66924c8a65f84761475175d1d8db947a42ff99992d9881649ceaec7045fe581910160405180910390a361117a565b6001600160a01b0383166000908152600560205260408120805484929061112e90849061169f565b909155505060408051838152602081018690526001600160a01b0385169188917f101f1a97913c28a4330dc8ff2f1f5251f4ef7008012d8f60bad8056958bf908b910160405180910390a35b505050505050565b60005460ff1661051557604051638dfc202b60e01b815260040160405180910390fd5b8280548282559060005260206000209081019282156111fa579160200282015b828111156111fa57825182546001600160a01b0319166001600160a01b039091161782556020909201916001909101906111c5565b5061120692915061120a565b5090565b5b80821115611206576000815560010161120b565b60006020828403121561123157600080fd5b5035919050565b80356001600160a01b038116811461124f57600080fd5b919050565b60008060006060848603121561126957600080fd5b833592506020840135915061128060408501611238565b90509250925092565b6000815180845260005b818110156112af57602081850181015186830182015201611293565b506000602082860101526020601f19601f83011685010191505092915050565b610140815260006112e461014083018d611289565b8b60208401528a60408401528281036060840152808a5180835260208301915060208c01925060005b818110156113345783516001600160a01b031683526020938401939092019160010161130d565b505080925050508760808301528660a083015261135560c083018715159052565b8460e083015283610100830152826101208301529b9a5050505050505050505050565b6000806040838503121561138b57600080fd5b8235915061139b60208401611238565b90509250929050565b610120815260006113b961012083018c611289565b602083019a909a525060408101979097526060870195909552608086019390935290151560a085015260c084015260e083015261010090910152919050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff81118282101715611437576114376113f8565b604052919050565b600082601f83011261145057600080fd5b813567ffffffffffffffff81111561146a5761146a6113f8565b8060051b61147a6020820161140e565b9182526020818501810192908101908684111561149657600080fd5b6020860192505b838310156114bf576114ae83611238565b82526020928301929091019061149d565b9695505050505050565b60008060008060008060c087890312156114e257600080fd5b863567ffffffffffffffff8111156114f957600080fd5b8701601f8101891361150a57600080fd5b803567ffffffffffffffff811115611524576115246113f8565b611537601f8201601f191660200161140e565b8181528a602083850101111561154c57600080fd5b8160208401602083013760006020928201830152975088013595505060408701359350606087013592506080870135915060a087013567ffffffffffffffff81111561159757600080fd5b6115a389828a0161143f565b9150509295509295509295565b600080604083850312156115c357600080fd5b50508035926020909101359150565b6000602082840312156115e457600080fd5b6115ed82611238565b9392505050565b60208082526025908201527f4f6e6c7920746865206f776e65722063616e2063616c6c20746869732066756e60408201526431ba34b7b760d91b606082015260800190565b600181811c9082168061164d57607f821691505b60208210810361166d57634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052603260045260246000fd5b634e487b7160e01b600052601160045260246000fd5b808201808211156106f4576106f4611689565b6000600182016116c4576116c4611689565b5060010190565b601f82111561171557806000526020600020601f840160051c810160208510156116f25750805b601f840160051c820191505b8181101561171257600081556001016116fe565b50505b505050565b815167ffffffffffffffff811115611734576117346113f8565b611748816117428454611639565b846116cb565b6020601f82116001811461177c57600083156117645750848201515b600019600385901b1c1916600184901b178455611712565b600084815260208120601f198516915b828110156117ac578785015182556020948501946001909201910161178c565b50848210156117ca5786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b80820281158282048414176106f4576106f4611689565b6020815260006115ed6020830184611289565b60008261182057634e487b7160e01b600052601260045260246000fd5b50069056fea26469706673582212203694871b9ba7b69c480be81f93e437b95de98dc4c9b459f9bcfd28034ee8b9a864736f6c634300081c0033", + "deployedBytecode": "0x6080604052600436106101095760003560e01c80637d9e10f511610095578063c1cbbca711610064578063c1cbbca714610317578063c66388221461032a578063c67e04d81461034a578063cf0b11171461036a578063f3f437031461038057600080fd5b80637d9e10f5146102755780638456cb59146102955780638da5cb5b146102aa578063bc5e0f30146102e257600080fd5b80633f4ba83a116100dc5780633f4ba83a146101cc57806345d5149f146101e15780635c975abb146101f75780635ce9284f1461020f57806364ccecde1461023f57600080fd5b806319cc1ec41461010e5780632cd19e62146101375780632f33afb6146101645780633ccfd60b146101b5575b600080fd5b34801561011a57600080fd5b5061012460085481565b6040519081526020015b60405180910390f35b34801561014357600080fd5b5061012461015236600461121f565b60036020526000908152604090205481565b34801561017057600080fd5b506101a561017f366004611254565b600260209081526000938452604080852082529284528284209052825290205460ff1681565b604051901515815260200161012e565b3480156101c157600080fd5b506101ca6103ad565b005b3480156101d857600080fd5b506101ca6104e3565b3480156101ed57600080fd5b5061012460075481565b34801561020357600080fd5b5060005460ff166101a5565b34801561021b57600080fd5b5061012461022a36600461121f565b60009081526001602052604090206003015490565b34801561024b57600080fd5b5061025f61025a36600461121f565b610517565b60405161012e9a999897969594939291906112cf565b34801561028157600080fd5b506101a5610290366004611378565b610685565b3480156102a157600080fd5b506101ca6106fa565b3480156102b657600080fd5b506006546102ca906001600160a01b031681565b6040516001600160a01b03909116815260200161012e565b3480156102ee57600080fd5b506103026102fd36600461121f565b61072c565b60405161012e999897969594939291906113a4565b6101ca61032536600461121f565b610801565b34801561033657600080fd5b506101ca6103453660046114c9565b610b48565b34801561035657600080fd5b506101ca6103653660046115b0565b610e4a565b34801561037657600080fd5b5061012460045481565b34801561038c57600080fd5b5061012461039b3660046115d2565b60056020526000908152604090205481565b6103b5610f0b565b336000908152600560205260409020548061040f5760405162461bcd60e51b8152602060048201526015602482015274139bc81c195b991a5b99c81dda5d1a191c985dd85b605a1b60448201526064015b60405180910390fd5b336000818152600560205260408082208290555190919083908381818185875af1925050503d8060008114610460576040519150601f19603f3d011682016040523d82523d6000602084013e610465565b606091505b50509050806104aa5760405162461bcd60e51b815260206004820152601160248201527015da5d1a191c985dd85b0819985a5b1959607a1b6044820152606401610406565b60405182815233907f7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b659060200160405180910390a25050565b6006546001600160a01b0316331461050d5760405162461bcd60e51b8152600401610406906115f4565b610515610f2f565b565b606060008060606000806000806000806000600160008d81526020019081526020016000209050806000018160010154826002015483600301846004015485600501548660060160009054906101000a900460ff1687600701548860080154896009015489805461058790611639565b80601f01602080910402602001604051908101604052809291908181526020018280546105b390611639565b80156106005780601f106105d557610100808354040283529160200191610600565b820191906000526020600020905b8154815290600101906020018083116105e357829003601f168201915b505050505099508680548060200260200160405190810160405280929190818152602001828054801561065c57602002820191906000526020600020905b81546001600160a01b0316815260019091019060200180831161063e575b505050505096509a509a509a509a509a509a509a509a509a509a50509193959799509193959799565b6000828152600160205260408120815b60038201548110156106ed57836001600160a01b03168260030182815481106106c0576106c0611673565b6000918252602090912001546001600160a01b0316036106e5576001925050506106f4565b600101610695565b5060009150505b92915050565b6006546001600160a01b031633146107245760405162461bcd60e51b8152600401610406906115f4565b610515610f81565b60016020526000908152604090208054819061074790611639565b80601f016020809104026020016040519081016040528092919081815260200182805461077390611639565b80156107c05780601f10610795576101008083540402835291602001916107c0565b820191906000526020600020905b8154815290600101906020018083116107a357829003601f168201915b50505060018401546002850154600486015460058701546006880154600789015460088a01546009909a015498999598949750929550909360ff9091169289565b610809610f0b565b6000818152600160205260409020600681015460ff166108625760405162461bcd60e51b8152602060048201526014602482015273436972636c65206973206e6f742061637469766560601b6044820152606401610406565b3460000361086e575050565b600081600701548260050154610884919061169f565b90506000826008015482610898919061169f565b4211905060008360010154905081156108bd5760098401546108ba908261169f565b90505b80341461090c5760405162461bcd60e51b815260206004820152601960248201527f57726f6e6720636f6e747269627574696f6e20616d6f756e74000000000000006044820152606401610406565b6000805b600386015481101561096657336001600160a01b031686600301828154811061093b5761093b611673565b6000918252602090912001546001600160a01b03160361095e5760019150610966565b600101610910565b50806109a35760405162461bcd60e51b815260206004820152600c60248201526b2737ba10309036b2b6b132b960a11b6044820152606401610406565b600086815260026020908152604080832060048901548452825280832033845290915290205460ff1615610a195760405162461bcd60e51b815260206004820152601760248201527f416c72656164792070616964207468697320726f756e640000000000000000006044820152606401610406565b60008681526002602090815260408083206004890154845282528083203384528252808320805460ff19166001179055888352600390915281208054349290610a6390849061169f565b90915550506004850154604080513481526020810192909252339188917ff9b7589b7a8d939b9da5dae90770378fe5fd7802be5d66432e85bedbdbfbcb7a910160405180910390a3600160005b6003870154811015610b2f57600088815260026020908152604080832060048b01548452909152812060038901805491929184908110610af257610af2611673565b60009182526020808320909101546001600160a01b0316835282019290925260400190205460ff16610b275760009150610b2f565b600101610ab0565b508015610b3f57610b3f87610fbe565b50505050505050565b610b50610f0b565b600281511015610ba25760405162461bcd60e51b815260206004820152601a60248201527f4d696e696d756d2032206d656d626572732072657175697265640000000000006044820152606401610406565b60005b8151811015610c7a576000610bbb82600161169f565b90505b8251811015610c7157828181518110610bd957610bd9611673565b60200260200101516001600160a01b0316838381518110610bfc57610bfc611673565b60200260200101516001600160a01b031603610c695760405162461bcd60e51b815260206004820152602660248201527f4475706c6963617465206d656d62657220616464726573736573206e6f7420616044820152651b1b1bddd95960d21b6064820152608401610406565b600101610bbe565b50600101610ba5565b50600754851015610cc45760405162461bcd60e51b8152602060048201526014602482015273436f6e747269627574696f6e20746f6f206c6f7760601b6044820152606401610406565b600854851115610d0e5760405162461bcd60e51b8152602060048201526015602482015274086dedce8e4d2c4eae8d2dedc40e8dede40d0d2ced605b1b6044820152606401610406565b6004805460009182610d1f836116b2565b909155506000818152600160205260409020909150610d3e888261171a565b50600081815260016020819052604090912001869055610d6185620151806117d9565b600082815260016020908152604090912060028101929092558351610d8c92600301918501906111a5565b506000818152600160208190526040822060048101929092554260058301556006909101805460ff19169091179055610dc885620151806117d9565b600082815260016020526040902060070155610de784620151806117d9565b600082815260016020526040908190206008810192909255600990910184905551339082907f6f657d36deb3b1aa2e949c5df40eac9d79359a73c2402cefa7aee870f9ceafc090610e39908b906117f0565b60405180910390a350505050505050565b6006546001600160a01b03163314610e745760405162461bcd60e51b8152600401610406906115f4565b80821115610ec45760405162461bcd60e51b815260206004820152601e60248201527f4d696e206c696d6974206d757374206265203c3d206d6178206c696d697400006044820152606401610406565b6007829055600881905560408051838152602081018390527f33aa92491b46a02a93d5a2523a688c5bb3f0192474ba74d63d506220094099ff910160405180910390a15050565b60005460ff16156105155760405163d93c066560e01b815260040160405180910390fd5b610f37611182565b6000805460ff191690557f5db9ee0a495bf2e6ff9c91a7834c1ba4fdd244a5e8aa4e537bd38aeae4b073aa335b6040516001600160a01b03909116815260200160405180910390a1565b610f89610f0b565b6000805460ff191660011790557f62e78cea01bee320cd4e420270b5ea74000d11b0c9f74754ebdbfc544b05a258610f643390565b6000818152600160205260408120600481015460038201805492939192610fe59084611803565b81548110610ff557610ff5611673565b60009182526020808320909101548683526003909152604082208054908390556004860180546001600160a01b0390931694509092611033836116b2565b9091555050426005850155600384015460048501540361105a5760068401805460ff191690555b6000826001600160a01b03168261c35090604051600060405180830381858888f193505050503d80600081146110ac576040519150601f19603f3d011682016040523d82523d6000602084013e6110b1565b606091505b5050905080156111065760408051838152602081018690526001600160a01b0385169188917fa66924c8a65f84761475175d1d8db947a42ff99992d9881649ceaec7045fe581910160405180910390a361117a565b6001600160a01b0383166000908152600560205260408120805484929061112e90849061169f565b909155505060408051838152602081018690526001600160a01b0385169188917f101f1a97913c28a4330dc8ff2f1f5251f4ef7008012d8f60bad8056958bf908b910160405180910390a35b505050505050565b60005460ff1661051557604051638dfc202b60e01b815260040160405180910390fd5b8280548282559060005260206000209081019282156111fa579160200282015b828111156111fa57825182546001600160a01b0319166001600160a01b039091161782556020909201916001909101906111c5565b5061120692915061120a565b5090565b5b80821115611206576000815560010161120b565b60006020828403121561123157600080fd5b5035919050565b80356001600160a01b038116811461124f57600080fd5b919050565b60008060006060848603121561126957600080fd5b833592506020840135915061128060408501611238565b90509250925092565b6000815180845260005b818110156112af57602081850181015186830182015201611293565b506000602082860101526020601f19601f83011685010191505092915050565b610140815260006112e461014083018d611289565b8b60208401528a60408401528281036060840152808a5180835260208301915060208c01925060005b818110156113345783516001600160a01b031683526020938401939092019160010161130d565b505080925050508760808301528660a083015261135560c083018715159052565b8460e083015283610100830152826101208301529b9a5050505050505050505050565b6000806040838503121561138b57600080fd5b8235915061139b60208401611238565b90509250929050565b610120815260006113b961012083018c611289565b602083019a909a525060408101979097526060870195909552608086019390935290151560a085015260c084015260e083015261010090910152919050565b634e487b7160e01b600052604160045260246000fd5b604051601f8201601f1916810167ffffffffffffffff81118282101715611437576114376113f8565b604052919050565b600082601f83011261145057600080fd5b813567ffffffffffffffff81111561146a5761146a6113f8565b8060051b61147a6020820161140e565b9182526020818501810192908101908684111561149657600080fd5b6020860192505b838310156114bf576114ae83611238565b82526020928301929091019061149d565b9695505050505050565b60008060008060008060c087890312156114e257600080fd5b863567ffffffffffffffff8111156114f957600080fd5b8701601f8101891361150a57600080fd5b803567ffffffffffffffff811115611524576115246113f8565b611537601f8201601f191660200161140e565b8181528a602083850101111561154c57600080fd5b8160208401602083013760006020928201830152975088013595505060408701359350606087013592506080870135915060a087013567ffffffffffffffff81111561159757600080fd5b6115a389828a0161143f565b9150509295509295509295565b600080604083850312156115c357600080fd5b50508035926020909101359150565b6000602082840312156115e457600080fd5b6115ed82611238565b9392505050565b60208082526025908201527f4f6e6c7920746865206f776e65722063616e2063616c6c20746869732066756e60408201526431ba34b7b760d91b606082015260800190565b600181811c9082168061164d57607f821691505b60208210810361166d57634e487b7160e01b600052602260045260246000fd5b50919050565b634e487b7160e01b600052603260045260246000fd5b634e487b7160e01b600052601160045260246000fd5b808201808211156106f4576106f4611689565b6000600182016116c4576116c4611689565b5060010190565b601f82111561171557806000526020600020601f840160051c810160208510156116f25750805b601f840160051c820191505b8181101561171257600081556001016116fe565b50505b505050565b815167ffffffffffffffff811115611734576117346113f8565b611748816117428454611639565b846116cb565b6020601f82116001811461177c57600083156117645750848201515b600019600385901b1c1916600184901b178455611712565b600084815260208120601f198516915b828110156117ac578785015182556020948501946001909201910161178c565b50848210156117ca5786840151600019600387901b60f8161c191681555b50505050600190811b01905550565b80820281158282048414176106f4576106f4611689565b6020815260006115ed6020830184611289565b60008261182057634e487b7160e01b600052601260045260246000fd5b50069056fea26469706673582212203694871b9ba7b69c480be81f93e437b95de98dc4c9b459f9bcfd28034ee8b9a864736f6c634300081c0033", "linkReferences": {}, "deployedLinkReferences": {} } diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 57eabd6..fd8e2af 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,17 +1,93 @@ import SusuChainJSON from "./abi.json"; -// Celo exports +// ────────────────────────────────────────────── +// Constants +// ────────────────────────────────────────────── + +/** Full Solidity ABI for the SusuChain contract on Celo */ export const SUSUCHAIN_CELO_ABI = SusuChainJSON.abi; + +/** Deployed SusuChain contract address on Celo Mainnet */ export const SUSUCHAIN_CELO_ADDRESS = "0x20B421Db767D3496E4489Db5C3122C1fD4625525"; -// Stacks exports +/** Deployed Stacks contract principal */ export const STACKS_CONTRACT_ADDRESS = "SP2T02XBVN9RAZ4360DSWF3JCG79B1QY2NR21RB0Q"; + +/** Stacks contract name */ export const STACKS_CONTRACT_NAME = "susuchain"; // Re-export Stacks network helpers import { STACKS_MAINNET } from "@stacks/network"; export const STACKS_NETWORK = STACKS_MAINNET; +// ────────────────────────────────────────────── +// TypeScript Interfaces +// ────────────────────────────────────────────── + +/** Options for building a Stacks create-circle transaction */ +export interface CreateCircleTxOptions { + senderKey: string; + name: string; + contributionMicroSTX: number; + members: string[]; + fee?: number; + nonce?: number; +} + +/** Options for building a Stacks contribute transaction */ +export interface ContributeTxOptions { + senderKey: string; + circleId: number; + fee?: number; + nonce?: number; +} + +/** Options for building a Stacks trigger-payout transaction */ +export interface TriggerPayoutTxOptions { + senderKey: string; + circleId: number; + fee?: number; + nonce?: number; +} + +/** Options for broadcasting a signed Stacks transaction */ +export interface BroadcastTxOptions { + transaction: StacksTransactionWire; +} + +/** Options for building Celo createCircle params */ +export interface CeloCreateCircleOptions { + name: string; + contributionWei: bigint; + roundDurationDays: number; + gracePeriodDays: number; + penaltyFee: bigint; + members: string[]; +} + +/** Options for building Celo contribute params */ +export interface CeloContributeOptions { + circleId: number; + valueWei: bigint; +} + +/** Options for building Celo hasPaid params */ +export interface CeloHasPaidOptions { + circleId: number; + round: number; + member: string; +} + +/** Options for building Celo isMember params */ +export interface CeloIsMemberOptions { + circleId: number; + user: string; +} + +// ────────────────────────────────────────────── +// Stacks Imports +// ────────────────────────────────────────────── + // Browser-based helper functions for Stacks (requires Leather wallet injected provider) import { openContractCall } from "@stacks/connect"; import { @@ -23,9 +99,14 @@ import { PostConditionMode, makeContractCall, broadcastTransaction, + fetchCallReadOnlyFunction, } from "@stacks/transactions"; import type { StacksTransactionWire, TxBroadcastResult } from "@stacks/transactions"; +// ────────────────────────────────────────────── +// Stacks Browser Wallet Helpers +// ────────────────────────────────────────────── + /** * Open Leather wallet to call create-circle * @param name Savings circle name (max 50 chars) @@ -99,14 +180,15 @@ export function callTriggerPayout( }); } -export async function buildCreateCircleTx(opts: { - senderKey: string; - name: string; - contributionMicroSTX: number; - members: string[]; - fee?: number; - nonce?: number; -}): Promise { +// ────────────────────────────────────────────── +// Stacks Server-Side Transaction Builders +// ────────────────────────────────────────────── + +/** + * Build and sign a create-circle transaction for server-side use + * @param opts Transaction options including senderKey + */ +export async function buildCreateCircleTx(opts: CreateCircleTxOptions): Promise { const tx = await makeContractCall({ contractAddress: STACKS_CONTRACT_ADDRESS, contractName: STACKS_CONTRACT_NAME, @@ -124,12 +206,11 @@ export async function buildCreateCircleTx(opts: { return tx; } -export async function buildContributeTx(opts: { - senderKey: string; - circleId: number; - fee?: number; - nonce?: number; -}): Promise { +/** + * Build and sign a contribute transaction for server-side use + * @param opts Transaction options including senderKey and circleId + */ +export async function buildContributeTx(opts: ContributeTxOptions): Promise { const tx = await makeContractCall({ contractAddress: STACKS_CONTRACT_ADDRESS, contractName: STACKS_CONTRACT_NAME, @@ -144,12 +225,11 @@ export async function buildContributeTx(opts: { return tx; } -export async function buildTriggerPayoutTx(opts: { - senderKey: string; - circleId: number; - fee?: number; - nonce?: number; -}): Promise { +/** + * Build and sign a trigger-payout transaction for server-side use + * @param opts Transaction options including senderKey and circleId + */ +export async function buildTriggerPayoutTx(opts: TriggerPayoutTxOptions): Promise { const tx = await makeContractCall({ contractAddress: STACKS_CONTRACT_ADDRESS, contractName: STACKS_CONTRACT_NAME, @@ -164,9 +244,11 @@ export async function buildTriggerPayoutTx(opts: { return tx; } -export async function broadcastTx(opts: { - transaction: StacksTransactionWire; -}): Promise { +/** + * Broadcast a signed Stacks transaction to the network + * @param opts Options containing the signed transaction + */ +export async function broadcastTx(opts: BroadcastTxOptions): Promise { const result = await broadcastTransaction({ transaction: opts.transaction, network: STACKS_NETWORK, @@ -174,14 +256,94 @@ export async function broadcastTx(opts: { return result; } -export function buildCeloCreateCircleParams(opts: { - name: string; - contributionWei: bigint; - roundDurationDays: number; - gracePeriodDays: number; - penaltyFee: bigint; - members: string[]; -}) { +// ────────────────────────────────────────────── +// Stacks Read-Only Call Builders +// ────────────────────────────────────────────── + +/** + * Call the read-only get-circle function on the Stacks contract + * @param circleId The ID of the circle to query + * @param senderAddress The Stacks address of the sender (for read-only calls) + */ +export async function readGetCircle(circleId: number, senderAddress: string) { + return fetchCallReadOnlyFunction({ + contractAddress: STACKS_CONTRACT_ADDRESS, + contractName: STACKS_CONTRACT_NAME, + functionName: "get-circle", + functionArgs: [uintCV(circleId)], + senderAddress, + network: STACKS_NETWORK, + }); +} + +/** + * Call the read-only get-circle-count function on the Stacks contract + * @param senderAddress The Stacks address of the sender (for read-only calls) + */ +export async function readGetCircleCount(senderAddress: string) { + return fetchCallReadOnlyFunction({ + contractAddress: STACKS_CONTRACT_ADDRESS, + contractName: STACKS_CONTRACT_NAME, + functionName: "get-circle-count", + functionArgs: [], + senderAddress, + network: STACKS_NETWORK, + }); +} + +/** + * Call the read-only has-member-paid function on the Stacks contract + * @param circleId The circle ID + * @param round The round number + * @param member The Stacks principal address of the member + * @param senderAddress The Stacks address of the sender (for read-only calls) + */ +export async function readHasMemberPaid( + circleId: number, + round: number, + member: string, + senderAddress: string +) { + return fetchCallReadOnlyFunction({ + contractAddress: STACKS_CONTRACT_ADDRESS, + contractName: STACKS_CONTRACT_NAME, + functionName: "has-member-paid", + functionArgs: [uintCV(circleId), uintCV(round), principalCV(member)], + senderAddress, + network: STACKS_NETWORK, + }); +} + +/** + * Call the read-only is-member function on the Stacks contract + * @param circleId The circle ID + * @param user The Stacks principal address to check + * @param senderAddress The Stacks address of the sender (for read-only calls) + */ +export async function readIsMember( + circleId: number, + user: string, + senderAddress: string +) { + return fetchCallReadOnlyFunction({ + contractAddress: STACKS_CONTRACT_ADDRESS, + contractName: STACKS_CONTRACT_NAME, + functionName: "is-member", + functionArgs: [uintCV(circleId), principalCV(user)], + senderAddress, + network: STACKS_NETWORK, + }); +} + +// ────────────────────────────────────────────── +// Celo Transaction Parameter Builders +// ────────────────────────────────────────────── + +/** + * Build viem-compatible params for createCircle on the Celo contract + * @param opts Circle creation options + */ +export function buildCeloCreateCircleParams(opts: CeloCreateCircleOptions) { return { address: SUSUCHAIN_CELO_ADDRESS as `0x${string}`, abi: SUSUCHAIN_CELO_ABI, @@ -197,10 +359,11 @@ export function buildCeloCreateCircleParams(opts: { }; } -export function buildCeloContributeParams(opts: { - circleId: number; - valueWei: bigint; -}) { +/** + * Build viem-compatible params for contribute on the Celo contract + * @param opts Contribute options including circleId and value + */ +export function buildCeloContributeParams(opts: CeloContributeOptions) { return { address: SUSUCHAIN_CELO_ADDRESS as `0x${string}`, abi: SUSUCHAIN_CELO_ABI, @@ -209,3 +372,106 @@ export function buildCeloContributeParams(opts: { value: opts.valueWei, }; } + +/** + * Build viem-compatible params for withdraw on the Celo contract. + * Withdraws any pending payouts that failed during automatic distribution. + */ +export function buildCeloWithdrawParams() { + return { + address: SUSUCHAIN_CELO_ADDRESS as `0x${string}`, + abi: SUSUCHAIN_CELO_ABI, + functionName: "withdraw" as const, + args: [], + }; +} + +/** + * Build viem-compatible params for reading circle details via getCircle + * @param circleId The circle ID to query + */ +export function buildCeloGetCircleParams(circleId: number) { + return { + address: SUSUCHAIN_CELO_ADDRESS as `0x${string}`, + abi: SUSUCHAIN_CELO_ABI, + functionName: "getCircle" as const, + args: [BigInt(circleId)], + }; +} + +/** + * Build viem-compatible params for checking if an address is a circle member + * @param opts Options including circleId and user address + */ +export function buildCeloIsMemberParams(opts: CeloIsMemberOptions) { + return { + address: SUSUCHAIN_CELO_ADDRESS as `0x${string}`, + abi: SUSUCHAIN_CELO_ABI, + functionName: "isMember" as const, + args: [BigInt(opts.circleId), opts.user as `0x${string}`], + }; +} + +/** + * Build viem-compatible params for getting the member count of a circle + * @param circleId The circle ID to query + */ +export function buildCeloGetMemberCountParams(circleId: number) { + return { + address: SUSUCHAIN_CELO_ADDRESS as `0x${string}`, + abi: SUSUCHAIN_CELO_ABI, + functionName: "getMemberCount" as const, + args: [BigInt(circleId)], + }; +} + +/** + * Build viem-compatible params for checking if a member has paid in a specific round + * @param opts Options including circleId, round, and member address + */ +export function buildCeloHasPaidParams(opts: CeloHasPaidOptions) { + return { + address: SUSUCHAIN_CELO_ADDRESS as `0x${string}`, + abi: SUSUCHAIN_CELO_ABI, + functionName: "hasPaid" as const, + args: [BigInt(opts.circleId), BigInt(opts.round), opts.member as `0x${string}`], + }; +} + +/** + * Build viem-compatible params for reading the total circle count + */ +export function buildCeloCircleCountParams() { + return { + address: SUSUCHAIN_CELO_ADDRESS as `0x${string}`, + abi: SUSUCHAIN_CELO_ABI, + functionName: "circleCount" as const, + args: [], + }; +} + +/** + * Build viem-compatible params for reading the round balance of a circle + * @param circleId The circle ID to query + */ +export function buildCeloRoundBalanceParams(circleId: number) { + return { + address: SUSUCHAIN_CELO_ADDRESS as `0x${string}`, + abi: SUSUCHAIN_CELO_ABI, + functionName: "roundBalance" as const, + args: [BigInt(circleId)], + }; +} + +/** + * Build viem-compatible params for reading pending withdrawals for an address + * @param account The address to check for pending withdrawals + */ +export function buildCeloPendingWithdrawalsParams(account: string) { + return { + address: SUSUCHAIN_CELO_ADDRESS as `0x${string}`, + abi: SUSUCHAIN_CELO_ABI, + functionName: "pendingWithdrawals" as const, + args: [account as `0x${string}`], + }; +} diff --git a/packages/sdk/test-cjs.js b/packages/sdk/test-cjs.js index fd64ded..5340778 100644 --- a/packages/sdk/test-cjs.js +++ b/packages/sdk/test-cjs.js @@ -1,14 +1,33 @@ const { SUSUCHAIN_CELO_ADDRESS, + SUSUCHAIN_CELO_ABI, STACKS_CONTRACT_NAME, + STACKS_CONTRACT_ADDRESS, + // Stacks server-side builders buildCreateCircleTx, buildContributeTx, buildTriggerPayoutTx, broadcastTx, + // Stacks read-only builders + readGetCircle, + readGetCircleCount, + readHasMemberPaid, + readIsMember, + // Celo param builders buildCeloCreateCircleParams, buildCeloContributeParams, + buildCeloWithdrawParams, + buildCeloGetCircleParams, + buildCeloIsMemberParams, + buildCeloGetMemberCountParams, + buildCeloHasPaidParams, + buildCeloCircleCountParams, + buildCeloRoundBalanceParams, + buildCeloPendingWithdrawalsParams, } = require("./dist/index.js"); +// ── Constants ── + if (SUSUCHAIN_CELO_ADDRESS !== "0x20B421Db767D3496E4489Db5C3122C1fD4625525") { throw new Error("Invalid Celo contract address exported in CommonJS module"); } @@ -16,19 +35,113 @@ if (STACKS_CONTRACT_NAME !== "susuchain") { throw new Error("Invalid Stacks contract name exported in CommonJS module"); } -const builders = { +// ── ABI Validation ── + +const createCircleABI = SUSUCHAIN_CELO_ABI.find( + (e) => e.name === "createCircle" && e.type === "function" +); +if (!createCircleABI || createCircleABI.inputs.length !== 6) { + throw new Error( + `Expected createCircle ABI to have 6 inputs, got ${createCircleABI ? createCircleABI.inputs.length : "not found"}` + ); +} +const withdrawABI = SUSUCHAIN_CELO_ABI.find( + (e) => e.name === "withdraw" && e.type === "function" +); +if (!withdrawABI) { + throw new Error("ABI is missing withdraw function"); +} + +// ── Stacks Server-Side Builders ── + +const stacksBuilders = { buildCreateCircleTx, buildContributeTx, buildTriggerPayoutTx, broadcastTx, +}; + +for (const [name, fn] of Object.entries(stacksBuilders)) { + if (typeof fn !== "function") { + throw new Error(`Expected ${name} to be a function, got ${typeof fn}`); + } +} + +// ── Stacks Read-Only Builders ── + +const stacksReaders = { + readGetCircle, + readGetCircleCount, + readHasMemberPaid, + readIsMember, +}; + +for (const [name, fn] of Object.entries(stacksReaders)) { + if (typeof fn !== "function") { + throw new Error(`Expected ${name} to be a function, got ${typeof fn}`); + } +} + +// ── Celo Param Builders ── + +const celoBuilders = { buildCeloCreateCircleParams, buildCeloContributeParams, + buildCeloWithdrawParams, + buildCeloGetCircleParams, + buildCeloIsMemberParams, + buildCeloGetMemberCountParams, + buildCeloHasPaidParams, + buildCeloCircleCountParams, + buildCeloRoundBalanceParams, + buildCeloPendingWithdrawalsParams, }; -for (const [name, fn] of Object.entries(builders)) { +for (const [name, fn] of Object.entries(celoBuilders)) { if (typeof fn !== "function") { throw new Error(`Expected ${name} to be a function, got ${typeof fn}`); } } +// ── Celo Builder Return Value Validation ── + +const createParams = buildCeloCreateCircleParams({ + name: "Test", + contributionWei: 1000000000000000000n, + roundDurationDays: 30, + gracePeriodDays: 3, + penaltyFee: 0n, + members: ["0x1234567890123456789012345678901234567890"], +}); +if (createParams.functionName !== "createCircle") { + throw new Error("buildCeloCreateCircleParams returned wrong functionName"); +} +if (createParams.args.length !== 6) { + throw new Error(`buildCeloCreateCircleParams returned ${createParams.args.length} args, expected 6`); +} + +const withdrawParams = buildCeloWithdrawParams(); +if (withdrawParams.functionName !== "withdraw") { + throw new Error("buildCeloWithdrawParams returned wrong functionName"); +} + +const circleCountParams = buildCeloCircleCountParams(); +if (circleCountParams.functionName !== "circleCount") { + throw new Error("buildCeloCircleCountParams returned wrong functionName"); +} + +const getCircleParams = buildCeloGetCircleParams(0); +if (getCircleParams.functionName !== "getCircle") { + throw new Error("buildCeloGetCircleParams returned wrong functionName"); +} + +const hasPaidParams = buildCeloHasPaidParams({ + circleId: 0, + round: 0, + member: "0x1234567890123456789012345678901234567890", +}); +if (hasPaidParams.functionName !== "hasPaid") { + throw new Error("buildCeloHasPaidParams returned wrong functionName"); +} + console.log("CommonJS exports validated successfully."); diff --git a/packages/sdk/test-esm.mjs b/packages/sdk/test-esm.mjs index 6421ad2..ca8768a 100644 --- a/packages/sdk/test-esm.mjs +++ b/packages/sdk/test-esm.mjs @@ -1,14 +1,33 @@ import { SUSUCHAIN_CELO_ADDRESS, + SUSUCHAIN_CELO_ABI, STACKS_CONTRACT_NAME, + STACKS_CONTRACT_ADDRESS, + // Stacks server-side builders buildCreateCircleTx, buildContributeTx, buildTriggerPayoutTx, broadcastTx, + // Stacks read-only builders + readGetCircle, + readGetCircleCount, + readHasMemberPaid, + readIsMember, + // Celo param builders buildCeloCreateCircleParams, buildCeloContributeParams, + buildCeloWithdrawParams, + buildCeloGetCircleParams, + buildCeloIsMemberParams, + buildCeloGetMemberCountParams, + buildCeloHasPaidParams, + buildCeloCircleCountParams, + buildCeloRoundBalanceParams, + buildCeloPendingWithdrawalsParams, } from "./dist/index.mjs"; +// ── Constants ── + if (SUSUCHAIN_CELO_ADDRESS !== "0x20B421Db767D3496E4489Db5C3122C1fD4625525") { throw new Error("Invalid Celo contract address exported in ES Module"); } @@ -16,19 +35,113 @@ if (STACKS_CONTRACT_NAME !== "susuchain") { throw new Error("Invalid Stacks contract name exported in ES Module"); } -const builders = { +// ── ABI Validation ── + +const createCircleABI = SUSUCHAIN_CELO_ABI.find( + (e) => e.name === "createCircle" && e.type === "function" +); +if (!createCircleABI || createCircleABI.inputs.length !== 6) { + throw new Error( + `Expected createCircle ABI to have 6 inputs, got ${createCircleABI ? createCircleABI.inputs.length : "not found"}` + ); +} +const withdrawABI = SUSUCHAIN_CELO_ABI.find( + (e) => e.name === "withdraw" && e.type === "function" +); +if (!withdrawABI) { + throw new Error("ABI is missing withdraw function"); +} + +// ── Stacks Server-Side Builders ── + +const stacksBuilders = { buildCreateCircleTx, buildContributeTx, buildTriggerPayoutTx, broadcastTx, +}; + +for (const [name, fn] of Object.entries(stacksBuilders)) { + if (typeof fn !== "function") { + throw new Error(`Expected ${name} to be a function, got ${typeof fn}`); + } +} + +// ── Stacks Read-Only Builders ── + +const stacksReaders = { + readGetCircle, + readGetCircleCount, + readHasMemberPaid, + readIsMember, +}; + +for (const [name, fn] of Object.entries(stacksReaders)) { + if (typeof fn !== "function") { + throw new Error(`Expected ${name} to be a function, got ${typeof fn}`); + } +} + +// ── Celo Param Builders ── + +const celoBuilders = { buildCeloCreateCircleParams, buildCeloContributeParams, + buildCeloWithdrawParams, + buildCeloGetCircleParams, + buildCeloIsMemberParams, + buildCeloGetMemberCountParams, + buildCeloHasPaidParams, + buildCeloCircleCountParams, + buildCeloRoundBalanceParams, + buildCeloPendingWithdrawalsParams, }; -for (const [name, fn] of Object.entries(builders)) { +for (const [name, fn] of Object.entries(celoBuilders)) { if (typeof fn !== "function") { throw new Error(`Expected ${name} to be a function, got ${typeof fn}`); } } +// ── Celo Builder Return Value Validation ── + +const createParams = buildCeloCreateCircleParams({ + name: "Test", + contributionWei: 1000000000000000000n, + roundDurationDays: 30, + gracePeriodDays: 3, + penaltyFee: 0n, + members: ["0x1234567890123456789012345678901234567890"], +}); +if (createParams.functionName !== "createCircle") { + throw new Error("buildCeloCreateCircleParams returned wrong functionName"); +} +if (createParams.args.length !== 6) { + throw new Error(`buildCeloCreateCircleParams returned ${createParams.args.length} args, expected 6`); +} + +const withdrawParams = buildCeloWithdrawParams(); +if (withdrawParams.functionName !== "withdraw") { + throw new Error("buildCeloWithdrawParams returned wrong functionName"); +} + +const circleCountParams = buildCeloCircleCountParams(); +if (circleCountParams.functionName !== "circleCount") { + throw new Error("buildCeloCircleCountParams returned wrong functionName"); +} + +const getCircleParams = buildCeloGetCircleParams(0); +if (getCircleParams.functionName !== "getCircle") { + throw new Error("buildCeloGetCircleParams returned wrong functionName"); +} + +const hasPaidParams = buildCeloHasPaidParams({ + circleId: 0, + round: 0, + member: "0x1234567890123456789012345678901234567890", +}); +if (hasPaidParams.functionName !== "hasPaid") { + throw new Error("buildCeloHasPaidParams returned wrong functionName"); +} + console.log("ES Module exports validated successfully."); From 29a84f72a36e691e5e6e1ee086bca94449aad123 Mon Sep 17 00:00:00 2001 From: Celo Composer Date: Tue, 16 Jun 2026 08:28:50 +0100 Subject: [PATCH 2/2] feat(web): add member input validation, duplicate checks, and rotation display --- apps/web/src/app/page.tsx | 219 +++++++++++++++++++++++++++++++++----- 1 file changed, 192 insertions(+), 27 deletions(-) diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 2748911..7582b79 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -29,6 +29,89 @@ const STACKS_ACCENT = "#fc6432"; const publicClient = createPublicClient({ chain: celo, transport: http() }); +interface ProcessedMembersResult { + members: string[]; + error?: string; +} + +function processCeloMembers(raw: string, creatorAddress?: string): ProcessedMembersResult { + if (!creatorAddress) { + return { members: [], error: "Wallet not connected" }; + } + + let list = raw + .split("\n") + .map((s) => s.trim()) + .filter(Boolean); + + // Validate format of input addresses + for (const m of list) { + if (!/^0x[a-fA-F0-9]{40}$/.test(m)) { + return { members: [], error: `Invalid Celo address format: "${m}"` }; + } + } + + const creatorClean = creatorAddress.toLowerCase(); + list = list.filter((m) => m.toLowerCase() !== creatorClean); + list.unshift(creatorAddress); + + // Check for duplicates + const lowercased = list.map((m) => m.toLowerCase()); + const unique = new Set(lowercased); + if (unique.size !== list.length) { + return { members: [], error: "Duplicate member addresses are not allowed" }; + } + + if (list.length < 2) { + return { members: [], error: "A circle must have at least 2 members (including yourself)" }; + } + + if (list.length > 10) { + return { members: [], error: "A circle can have a maximum of 10 members" }; + } + + return { members: list }; +} + +function processStacksMembers(raw: string, creatorAddress?: string): ProcessedMembersResult { + if (!creatorAddress) { + return { members: [], error: "Wallet not connected" }; + } + + let list = raw + .split("\n") + .map((s) => s.trim()) + .filter(Boolean); + + // Validate format of input addresses + for (const m of list) { + if (!/^S[PMT][0-9a-zA-Z]{37,42}$/.test(m)) { + return { members: [], error: `Invalid Stacks address format: "${m}"` }; + } + } + + const creatorClean = creatorAddress.toLowerCase(); + list = list.filter((m) => m.toLowerCase() !== creatorClean); + list.unshift(creatorAddress); + + // Check for duplicates + const lowercased = list.map((m) => m.toLowerCase()); + const unique = new Set(lowercased); + if (unique.size !== list.length) { + return { members: [], error: "Duplicate member addresses are not allowed" }; + } + + if (list.length < 2) { + return { members: [], error: "A circle must have at least 2 members (including yourself)" }; + } + + if (list.length > 10) { + return { members: [], error: "A circle can have a maximum of 10 members" }; + } + + return { members: list }; +} + export default function Home() { // --- Global State --- const [activeChain, setActiveChain] = useState<"celo" | "stacks">("celo"); @@ -60,6 +143,7 @@ export default function Home() { isOpen: boolean; title: string; details: { label: string; value: string }[]; + memberRotation?: string[]; estimatedFee: string; isLoadingFee: boolean; onConfirm: () => Promise | void; @@ -97,14 +181,10 @@ export default function Home() { const accent = activeChain === "celo" ? CELO_ACCENT : STACKS_ACCENT; // --- Celo Handlers --- - const handleCeloCreate = async () => { + const handleCeloCreate = async (validatedMembers: string[]) => { try { setCeloStatus("⏳ Submitting..."); const weiAmount = parseUnits(contributionCelo, 18); - const memberList = membersRaw - .split("\n") - .map((s) => s.trim()) - .filter(Boolean); const walletClient = createWalletClient({ chain: celo, transport: custom(window.ethereum as any), @@ -113,7 +193,7 @@ export default function Home() { address: SUSUCHAIN_CELO_ADDRESS, abi: SUSUCHAIN_CELO_ABI, functionName: "createCircle", - args: [circleName, weiAmount, BigInt(cycleDays), memberList], + args: [circleName, weiAmount, BigInt(cycleDays), validatedMembers], account: address as `0x${string}`, }); setCeloStatus(`✅ TX: ${hash}`); @@ -123,7 +203,7 @@ export default function Home() { chain: "celo", contractAddress: SUSUCHAIN_CELO_ADDRESS, functionName: "createCircle", - arguments: [circleName, contributionCelo, cycleDays, membersRaw], + arguments: [circleName, contributionCelo, cycleDays, validatedMembers.join("\n")], account: address || undefined, }); } @@ -219,10 +299,12 @@ export default function Home() { return; } - const memberList = membersRaw - .split("\n") - .map((s) => s.trim()) - .filter(Boolean); + const res = processCeloMembers(membersRaw, address); + if (res.error) { + setCeloStatus(`❌ ${res.error}`); + return; + } + const validatedMembers = res.members; setModalConfig({ isOpen: true, @@ -231,13 +313,14 @@ export default function Home() { { label: "Circle Name", value: circleName }, { label: "Contribution Amount", value: `${contributionCelo} CELO` }, { label: "Cycle Duration", value: `${cycleDays} days` }, - { label: "Total Members", value: `${memberList.length} addresses` }, + { label: "Total Members", value: `${validatedMembers.length} addresses` }, ], + memberRotation: validatedMembers, estimatedFee: "Estimating fee...", isLoadingFee: true, onConfirm: async () => { setModalConfig(null); - await handleCeloCreate(); + await handleCeloCreate(validatedMembers); }, }); @@ -247,7 +330,7 @@ export default function Home() { address: SUSUCHAIN_CELO_ADDRESS, abi: SUSUCHAIN_CELO_ABI, functionName: "createCircle", - args: [circleName, weiAmount, BigInt(cycleDays), memberList], + args: [circleName, weiAmount, BigInt(cycleDays), validatedMembers], account: address as `0x${string}`, }); const gasPrice = await publicClient.getGasPrice(); @@ -332,14 +415,10 @@ export default function Home() { }; // --- Stacks Handlers --- - const handleStacksCreate = () => { + const handleStacksCreate = (validatedMembers: string[]) => { try { const microSTX = Math.floor(parseFloat(sContribution) * 1_000_000); - const memberList = sMembers - .split("\n") - .map((s) => s.trim()) - .filter(Boolean); - callCreateCircle(sName, microSTX, memberList, (data: any) => { + callCreateCircle(sName, microSTX, validatedMembers, (data: any) => { setSStatus( `✅ TX: ${data.txId} — link: https://explorer.hiro.so/txid/${data.txId}` ); @@ -350,7 +429,7 @@ export default function Home() { chain: "stacks", contractAddress: STACKS_CONTRACT_ADDRESS, functionName: "create-circle", - arguments: [sName, sContribution, sMembers], + arguments: [sName, sContribution, validatedMembers.join("\n")], account: stacksAddress || undefined, }); } @@ -395,23 +474,27 @@ export default function Home() { setSStatus("❌ Please fill in all fields"); return; } - const memberList = sMembers - .split("\n") - .map((s) => s.trim()) - .filter(Boolean); + const res = processStacksMembers(sMembers, stacksAddress); + if (res.error) { + setSStatus(`❌ ${res.error}`); + return; + } + const validatedMembers = res.members; + setModalConfig({ isOpen: true, title: "Confirm Stacks circle creation", details: [ { label: "Circle Name", value: sName }, { label: "Contribution Amount", value: `${sContribution} STX` }, - { label: "Total Members", value: `${memberList.length} addresses` }, + { label: "Total Members", value: `${validatedMembers.length} addresses` }, ], + memberRotation: validatedMembers, estimatedFee: "0.001800 STX", isLoadingFee: false, onConfirm: () => { setModalConfig(null); - handleStacksCreate(); + handleStacksCreate(validatedMembers); }, }); }; @@ -961,6 +1044,42 @@ export default function Home() { ))} + {modalConfig.memberRotation && modalConfig.memberRotation.length > 0 && ( +
+
Rotation / Payout Order
+
+ {modalConfig.memberRotation.map((member, idx) => { + const isCelo = activeChain === "celo"; + const accentColor = isCelo ? CELO_ACCENT : STACKS_ACCENT; + const badgeBg = isCelo ? "rgba(252, 255, 82, 0.1)" : "rgba(252, 100, 50, 0.1)"; + const badgeBorder = isCelo ? "rgba(252, 255, 82, 0.2)" : "rgba(252, 100, 50, 0.2)"; + const isCreator = idx === 0; + + return ( +
+ {idx + 1}. + + {truncate(member)} + + {isCreator && ( + + Creator + + )} +
+ ); + })} +
+
+ )} +
Estimated Network Fee
@@ -1339,6 +1458,52 @@ const styles: Record = { cursor: "pointer", transition: "all 0.2s", }, + modalRotationSection: { + backgroundColor: "#1a1a1a", + border: "1px solid #222", + borderRadius: 8, + padding: 14, + marginBottom: 20, + }, + modalRotationTitle: { + fontSize: 11, + fontWeight: 700, + color: "#a3a3a3", + textTransform: "uppercase" as const, + letterSpacing: "0.05em", + marginTop: 0, + marginBottom: 10, + }, + modalRotationList: { + display: "flex", + flexDirection: "column" as const, + gap: 6, + }, + modalRotationItem: { + display: "flex", + alignItems: "center", + gap: 8, + fontSize: 13, + }, + modalRotationIndex: { + fontFamily: "monospace", + color: "#9ca3af", + fontWeight: 700, + width: 16, + }, + modalRotationAddr: { + fontFamily: "monospace", + color: "#fff", + flex: 1, + }, + modalRotationBadge: { + fontSize: 9, + fontWeight: 700, + textTransform: "uppercase" as const, + padding: "2px 6px", + borderRadius: 4, + border: "1px solid", + }, footer: { marginTop: "auto", paddingTop: 40,