From 51d3b7969e1015813144891007de3d94b4f0b18e Mon Sep 17 00:00:00 2001 From: Thomas Greco Date: Sun, 8 Dec 2024 17:10:35 -0500 Subject: [PATCH 1/6] feat: added code for enforcing correct offerArgs shape --- contract/src/airdrop.contract.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/contract/src/airdrop.contract.js b/contract/src/airdrop.contract.js index 8abd338..b91de1f 100644 --- a/contract/src/airdrop.contract.js +++ b/contract/src/airdrop.contract.js @@ -11,7 +11,7 @@ import { withdrawFromSeat, } from '@agoric/zoe/src/contractSupport/index.js'; import { divideBy } from '@agoric/zoe/src/contractSupport/ratio.js'; -import { makeTracer } from '@agoric/internal'; +import { makeTracer, mustMatch } from '@agoric/internal'; import { makeWaker, oneDay } from './helpers/time.js'; import { handleFirstIncarnation, @@ -23,6 +23,18 @@ import { objectToMap } from './helpers/objectTools.js'; import { getMerkleRootFromMerkleProof } from './merkle-tree/index.js'; import '@agoric/zoe/exported.js'; +const ProofDataShape = harden({ + hash: M.string(), + direction: M.string(), +}); + +const OfferArgsShape = harden({ + tier: M.number(), + address: M.string(), + key: M.string(), + proof: M.arrayOf(ProofDataShape), +}); + const TT = makeTracer('ContractStartFn'); export const messagesObject = { @@ -345,12 +357,16 @@ export const start = async (zcf, privateArgs, baggage) => { * }} offerArgs */ const claimHandler = (claimSeat, offerArgs) => { + mustMatch( + offerArgs, + OfferArgsShape, + 'offerArgs does not contain the correct data.', + ); const { give: { Fee: claimTokensFee }, } = claimSeat.getProposal(); const { proof, key: pubkey, address, tier } = offerArgs; - // This line was added because of issues when testing // Is there a way to gracefully test assertion failures???? if (accountStore.has(pubkey)) { From a0c1eadd61a355cceebece5cdbb1c17891e6f8ce Mon Sep 17 00:00:00 2001 From: Thomas Greco <6646552+tgrecojs@users.noreply.github.com> Date: Thu, 12 Dec 2024 20:23:57 -0500 Subject: [PATCH 2/6] feat: codespace config --- .devcontainer/devcontainer.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..6912fcd --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,6 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + "ghcr.io/devcontainers/features/node:1": {} + } +} From d7d6d7259574b8fb9b64d3ac18f75e8ad5c1893a Mon Sep 17 00:00:00 2001 From: Thomas Greco <6646552+tgrecojs@users.noreply.github.com> Date: Fri, 13 Dec 2024 12:45:19 +0000 Subject: [PATCH 3/6] feat(depositFacet-claim): working implementation of using depositFacet to claim --- contract/src/airdrop.contract.js | 122 +++++++++++++-- contract/src/airdrop.local.proposal.js | 5 +- contract/src/fixHub.js | 2 +- .../test/tribbles-airdrop/contract.test.js | 38 ++++- contract/test/tribbles-airdrop/e2e.test.js | 142 ++++++++++++++---- contract/tools/boot-tools.js | 23 ++- 6 files changed, 282 insertions(+), 50 deletions(-) diff --git a/contract/src/airdrop.contract.js b/contract/src/airdrop.contract.js index 8abd338..5c815cb 100644 --- a/contract/src/airdrop.contract.js +++ b/contract/src/airdrop.contract.js @@ -5,11 +5,15 @@ import { E } from '@endo/far'; import { AmountMath, AmountShape, AssetKind, MintShape } from '@agoric/ertp'; import { TimeMath } from '@agoric/time'; import { TimerShape } from '@agoric/zoe/src/typeGuards.js'; +import { bech32 } from 'bech32'; +import { sha256 } from '@noble/hashes/sha256'; +import { ripemd160 } from '@noble/hashes/ripemd160'; import { atomicRearrange, makeRatio, withdrawFromSeat, } from '@agoric/zoe/src/contractSupport/index.js'; +import { decodeBase64 } from '@endo/base64'; import { divideBy } from '@agoric/zoe/src/contractSupport/ratio.js'; import { makeTracer } from '@agoric/internal'; import { makeWaker, oneDay } from './helpers/time.js'; @@ -23,6 +27,38 @@ import { objectToMap } from './helpers/objectTools.js'; import { getMerkleRootFromMerkleProof } from './merkle-tree/index.js'; import '@agoric/zoe/exported.js'; +const compose = + (...fns) => + args => + fns.reduceRight((x, f) => f(x), args); +const toAgoricBech = (data, limit) => + bech32.encode('agoric', bech32.toWords(data), limit); + +/** + * Creates a digest function for a given hash function. + * + * @param {object} hashFn - The hash function object (e.g., sha256, ripemd160). It must implement `create()` and the resulting object must implement `update()` and `digest()`. + * @returns {function(Uint8Array): Uint8Array} - A function that takes data and returns the digest. + */ +const createDigest = + hashFn => + /** + * @param {Uint8Array} data - The data to hash. + * @returns {Uint8Array} - The hash digest. + */ + data => + hashFn.create().update(data).digest(); + +const createSha256Digest = createDigest(sha256); +const createRipe160Digest = createDigest(ripemd160); + +const computeAddress = compose( + toAgoricBech, + createRipe160Digest, + createSha256Digest, + decodeBase64, +); + const TT = makeTracer('ContractStartFn'); export const messagesObject = { @@ -70,14 +106,13 @@ harden(RESTARTING); /** @import {ContractMeta} from './@types/zoe-contract-facet.d'; */ /** @import {Remotable} from '@endo/marshal' */ -export const privateArgsShape = harden({ - marshaller: M.remotable('marshaller'), - storageNode: M.remotable('chainStorageNode'), +export const privateArgsShape = { + namesByAddress: M.remotable('marshaller'), timer: TimerShape, -}); +}; harden(privateArgsShape); -export const customTermsShape = harden({ +export const customTermsShape = { targetEpochLength: M.bigint(), initialPayoutValues: M.arrayOf(M.bigint()), tokenName: M.string(), @@ -86,7 +121,7 @@ export const customTermsShape = harden({ startTime: M.bigint(), feeAmount: AmountShape, merkleRoot: M.string(), -}); +}; harden(customTermsShape); export const divideAmountByTwo = brand => amount => @@ -162,7 +197,27 @@ export const start = async (zcf, privateArgs, baggage) => { /** @type {Zone} */ const zone = makeDurableZone(baggage, 'rootZone'); - const { timer } = privateArgs; + const { timer, namesByAddress } = privateArgs; + + /** + * @param {string} addr + * @returns {ERef} + */ + const getDepositFacet = addr => { + assert.typeof(addr, 'string'); + console.log('geting deposit facet for::', addr); + const df = E(namesByAddress).lookup(addr, 'depositFacet'); + console.log('------------------------'); + console.log('df::', df); + return df; + }; + + /** + * @param {string} addr + * @param {Payment} pmt + */ + const sendTo = (addr, pmt) => E(getDepositFacet(addr)).receive(pmt); + /** @type {ContractTerms} */ const { startTime = 120n, @@ -344,19 +399,21 @@ export const start = async (zcf, privateArgs, baggage) => { * tier: number; * }} offerArgs */ - const claimHandler = (claimSeat, offerArgs) => { + const claimHandler = async (claimSeat, offerArgs) => { const { give: { Fee: claimTokensFee }, } = claimSeat.getProposal(); - const { proof, key: pubkey, address, tier } = offerArgs; + const { proof, key: pubkey, tier } = offerArgs; + + const derivedAddress = computeAddress(pubkey); // This line was added because of issues when testing // Is there a way to gracefully test assertion failures???? if (accountStore.has(pubkey)) { claimSeat.exit(); throw new Error( - `Allocation for address ${address} has already been claimed.`, + `Allocation for address ${derivedAddress} has already been claimed.`, ); } @@ -367,21 +424,54 @@ export const start = async (zcf, privateArgs, baggage) => { ); const paymentAmount = this.state.payoutArray[tier]; + console.log('------------------------'); + console.log('paymentAmount::', paymentAmount); + const payment = await withdrawFromSeat(zcf, tokenHolderSeat, { + Tokens: paymentAmount, + }); + + console.log('------------------------'); + console.log( + 'withdrawn from tokenHolderSeat ### payment::', + payment, + ); + console.log('------------------------'); + + const depositFacet = await getDepositFacet(derivedAddress); + + console.log('------------------------'); + console.log( + `depositFacet:: AFTER getDepositFacet(${derivedAddress})`, + depositFacet, + ); + await Promise.all( + Object.values(payment).map(pmtP => + E.when(pmtP, pmt => E(depositFacet).receive(pmt)), + ), + ); + // XXX partial failure? return payments? + // await Promise.all( + // E.when( + // payment, + // E(depositFacet).receive(payment), + // new Error('Error getting payment'), + // ), + // ); + TT( + 'After await E.when(payment, pmy => E(depositFacet).recieve(pmt)', + ); rearrange( - harden([ - [tokenHolderSeat, claimSeat, { Tokens: paymentAmount }], - [claimSeat, tokenHolderSeat, { Fee: claimTokensFee }], - ]), + harden([[claimSeat, tokenHolderSeat, { Fee: claimTokensFee }]]), ); claimSeat.exit(); accountStore.add(pubkey, { - address, + address: derivedAddress, pubkey, tier, - amountAllocated: paymentAmount, + amountAllocated: payment.value, epoch: this.state.currentEpoch, }); diff --git a/contract/src/airdrop.local.proposal.js b/contract/src/airdrop.local.proposal.js index 3d2b777..617add5 100644 --- a/contract/src/airdrop.local.proposal.js +++ b/contract/src/airdrop.local.proposal.js @@ -4,6 +4,7 @@ import { Fail } from '@endo/errors'; import { makeMarshal } from '@endo/marshal'; import { makeTracer } from '@agoric/internal'; import { installContract } from './platform-goals/start-contract.js'; +import { fixHub } from './fixHub.js'; import './types.js'; const contractName = 'tribblesAirdrop'; @@ -111,7 +112,7 @@ export const startAirdrop = async (powers, config) => { const { consume: { namesByAddressAdmin, - namesByAddress, + // namesByAddress, // bankManager, board, chainTimerService, @@ -152,6 +153,7 @@ export const startAirdrop = async (powers, config) => { customTerms?.merkleRoot, 'can not start contract without merkleRoot???', ); + const namesByAddress = await fixHub(namesByAddressAdmin); const installation = await installContract(powers, { name: contractName, @@ -168,6 +170,7 @@ export const startAirdrop = async (powers, config) => { issuerNames: ['Tribbles'], privateArgs: harden({ timer, + namesByAddress, }), }; trace('BEFORE astartContract(permittedPowers, startOpts);', { startOpts }); diff --git a/contract/src/fixHub.js b/contract/src/fixHub.js index 1583c83..6139a52 100644 --- a/contract/src/fixHub.js +++ b/contract/src/fixHub.js @@ -6,7 +6,7 @@ const { Fail } = assert; /** * ref https://github.com/Agoric/agoric-sdk/issues/8408#issuecomment-1741445458 * - * @param {ERef} namesByAddressAdmin + * @param {import('@agoric/vats').NameAdmin} namesByAddressAdmin */ export const fixHub = async namesByAddressAdmin => { assert(namesByAddressAdmin, 'no namesByAddressAdmin???'); diff --git a/contract/test/tribbles-airdrop/contract.test.js b/contract/test/tribbles-airdrop/contract.test.js index 95667c4..1ed2787 100644 --- a/contract/test/tribbles-airdrop/contract.test.js +++ b/contract/test/tribbles-airdrop/contract.test.js @@ -13,7 +13,7 @@ import { makeStableFaucet } from '../mintStable.js'; import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; import { oneDay, TimeIntervals } from '../../src/helpers/time.js'; import { extract } from '@agoric/vats/src/core/utils.js'; -import { mockBootstrapPowers } from '../../tools/boot-tools.js'; +import { makeMockTools, mockBootstrapPowers } from '../../tools/boot-tools.js'; import { getBundleId } from '../../tools/bundle-tools.js'; import { head } from '../../src/helpers/objectTools.js'; @@ -84,6 +84,8 @@ const makeTestContext = async t => { console.log('invitationIssuer::', invitationIssuer); const bundleCache = await makeNodeBundleCache('bundles/', {}, s => import(s)); const bundle = await bundleCache.load(contractPath, 'assetContract'); + const {} = makeMockTools(t, bundleCache); + const testFeeIssuer = await E(zoe).getFeeIssuer(); const testFeeBrand = await E(testFeeIssuer).getBrand(); @@ -96,7 +98,6 @@ const makeTestContext = async t => { return { invitationIssuer, zoe, - invitationIssuer, bundle, bundleCache, makeLocalTimer, @@ -107,7 +108,38 @@ const makeTestContext = async t => { }; }; -test.before(async t => (t.context = await makeTestContext(t))); +test.before('before hook', async t => { + const { admin, vatAdminState } = makeFakeVatAdmin(); + const { zoeService: zoe, feeMintAccess } = makeZoeKitForTest(admin); + + const invitationIssuer = zoe.getInvitationIssuer(); + console.log('------------------------'); + console.log('invitationIssuer::', invitationIssuer); + const bundleCache = await makeNodeBundleCache('bundles/', {}, s => import(s)); + const bundle = await bundleCache.load(contractPath, 'assetContract'); + const {} = makeMockTools(t, bundleCache); + + const testFeeIssuer = await E(zoe).getFeeIssuer(); + const testFeeBrand = await E(testFeeIssuer).getBrand(); + + const testFeeTokenFaucet = await makeStableFaucet({ + feeMintAccess, + zoe, + bundleCache, + }); + console.log('bundle:::', { bundle, bundleCache }); + t.context = { + invitationIssuer, + zoe, + bundle, + bundleCache, + makeLocalTimer, + testFeeTokenFaucet, + faucet: testFeeTokenFaucet.faucet, + testFeeBrand, + testFeeIssuer, + }; +}); // IDEA: use test.serial and pass work products // between tests using t.context. diff --git a/contract/test/tribbles-airdrop/e2e.test.js b/contract/test/tribbles-airdrop/e2e.test.js index dcf251f..30d910d 100644 --- a/contract/test/tribbles-airdrop/e2e.test.js +++ b/contract/test/tribbles-airdrop/e2e.test.js @@ -29,9 +29,10 @@ import { import { makeMockTools, mockBootstrapPowers } from '../../tools/boot-tools.js'; import { merkleTreeAPI } from '../../src/merkle-tree/index.js'; import { makeStableFaucet } from '../mintStable.js'; -import { simulateClaim } from './actors.js'; +import { makeOfferArgs, simulateClaim } from './actors.js'; import { oneDay } from '../../src/helpers/time.js'; import { merkleTreeObj } from './generated_keys.js'; +import { AmountMath } from '@agoric/ertp'; const { accounts } = merkleTreeObj; // import { makeAgdTools } from '../agd-tools.js'; @@ -176,11 +177,21 @@ test.serial('deploy contract with core eval: airdrop / airdrop', async t => { // const { tribblesAirdrop } = t.context.shared.bundles; // t.deepEqual(await checkBundle(tribblesAirdrop), ''); // }); - +const makeMakeOfferSpec = instance => (account, feeAmount) => ({ + id: `${account.address}-offer-${Date.now()}`, + invitationSpec: { + source: 'contract', + instance, + publicInvitationMaker: 'makeClaimTokensInvitation', + }, + proposal: { give: { Fee: feeAmount } }, + offerArgs: { ...makeOfferArgs(account) }, +}); test.serial('E2E test', async t => { const merkleRoot = merkleTreeAPI.generateMerkleRoot( accounts.map(x => x.pubkey.key), ); + const { bundleCache } = t.context; t.log('starting contract with merkleRoot:', merkleRoot); // Is there a better way to obtain a reference to this bundle??? @@ -188,7 +199,10 @@ test.serial('E2E test', async t => { const { tribblesAirdrop } = t.context.shared.bundles; const bundleID = getBundleId(tribblesAirdrop); - const { powers, vatAdminState } = await mockBootstrapPowers(t.log); + const { powers, vatAdminState, makeMockWalletFactory } = await makeMockTools( + t, + bundleCache, + ); const { feeMintAccess, zoe, chainTimerService } = powers.consume; vatAdminState.installBundle(bundleID, tribblesAirdrop); @@ -210,9 +224,33 @@ test.serial('E2E test', async t => { const airdropSpace = powers; const instance = await airdropSpace.instance.consume.tribblesAirdrop; + const terms = await E(zoe).getTerms(instance); + const { issuers, brands } = terms; + + const walletFactory = makeMockWalletFactory({ + Tribbles: issuers.Tribbles, + Fee: issuers.Fee, + }); + + console.log('BRANDS::', brands); + const wallets = { + alice: await walletFactory.makeSmartWallet(accounts[4].address), + bob: await walletFactory.makeSmartWallet(accounts[2].address), + }; + const { faucet, mintBrandedPayment } = makeStableFaucet({ + bundleCache, + feeMintAccess, + zoe, + }); + + await Object.values(wallets).map(async wallet => { + const pmt = await mintBrandedPayment(10n); + console.log('payment::', pmt); + await E(wallet.deposit).receive(pmt); + }); + const makeOfferSpec = makeMakeOfferSpec(instance); + // Now that we have the instance, resume testing as above. - const { bundleCache } = t.context; - const { faucet } = makeStableFaucet({ bundleCache, feeMintAccess, zoe }); // TODO: update simulateClaim to construct claimArgs object. // see makeOfferArgs function for reference. @@ -234,34 +272,82 @@ test.serial('E2E test', async t => { // ); await E(chainTimerService).advanceBy(oneDay * (oneDay / 2n)); + const makeFeeAmount = () => AmountMath.make(brands.Fee, 5n); + + /** + * @summary this creates an AsyncGenerator object that emits values in the code blocks that follow. + * + * @example the first value is recieved as a result of the following code + * + * await E(aliceUpdates).next() + * .then(res => { + * console.log('update res', res); + * return res; + * }); + */ + const aliceUpdates = E(wallets.alice.offers).executeOffer( + makeOfferSpec(accounts[4], makeFeeAmount()), + ); - await simulateClaim( - t, - zoe, - instance, - await faucet(5n * 1_000_000n), - accounts[4], + /* + * + * { + * value: { + * updated: 'offerStatus', + * status: { + * id: 'agoric19s266pak0llft2gcapn64x5aa37ysqnqzky46y-offer-1734092744506', + * invitationSpec: [Object], + * proposal: [Object], + * offerArgs: [Object] + * } + * }, + * done: false + * } + */ + /** + * @typedef {{value: { updated: string, status: { id: string, invitationSpec: import('../../tools/wallet-tools.js').InvitationSpec, proposal:Proposal, offerArgs: {key: string, proof: []}}}}} OfferResult + */ + + /** @returns {OfferResult} */ + await E(aliceUpdates) + .next() + .then(res => { + console.log('update res', res); + return res; + }); + + const tribblesWatcher = await E(wallets.alice.peek).purseUpdates( + brands.Tribbles, ); - await simulateClaim( - t, - zoe, - instance, - await faucet(5n * 1_000_000n), - accounts[4], + let payout; + for await (const value of tribblesWatcher) { + console.log('update from smartWallet', value); // Process the value here + payout = value; + } + t.deepEqual( + AmountMath.isGTE(payout, AmountMath.make(brands.Tribbles, 0n)), true, - `Allocation for address ${accounts[4].address} has already been claimed.`, ); + // await simulateClaim( + // t, + // zoe, + // instance, + // await faucet(5n * 1_000_000n), + // accounts[4], + // true, + // `Allocation for address ${accounts[4].address} has already been claimed.`, + // ); }); -test.serial('agoricNames.instances has contract: airdrop', async t => { - const { makeQueryTool } = t.context; - const hub0 = makeAgoricNames(makeQueryTool()); +// test.serial('agoricNames.instances has contract: airdrop', async t => { +// const { makeQueryTool } = t.context; +// const hub0 = makeAgoricNames(makeQueryTool()); - const agoricNames = makeNameProxy(hub0); - console.log({ agoricNames }); - await null; - const instance = await E(agoricNames).lookup('instance', 'tribblesAirdrop'); - t.is(passStyleOf(instance), 'remotable'); - t.log(instance); -}); +// const agoricNames = makeNameProxy(hub0); +// console.log({ agoricNames }); +// await null; +// const instance = await E(agoricNames).lookup('instance', 'tribblesAirdrop'); +// t.is(passStyleOf(instance), 'remotable'); +// t.log(instance); +// }); diff --git a/contract/tools/boot-tools.js b/contract/tools/boot-tools.js index 19d498d..110cc23 100644 --- a/contract/tools/boot-tools.js +++ b/contract/tools/boot-tools.js @@ -215,7 +215,25 @@ export const makeMockTools = async (t, bundleCache) => { { namesByAddressAdmin, zoe }, smartWalletIssuers, ); - + const makeMockWalletFactory = (additionalIssuers = {}) => { + const newIssuers = {}; + for (const [name, kit] of entries(additionalIssuers)) { + console.group( + `::: START::: ADDING NEW ISSUER ${name.toUpperCase()} TO SMART WALLET ISSUERS::: START::: `, + ); + powers.issuer.produce[name].resolve(kit.issuer); + powers.brand.produce[name].resolve(kit.brand); + console.log( + `::: END::: ADDING NEW ISSUER ${name.toUpperCase()} TO SMART WALLET ISSUERS::: END::: `, + ); + Object.assign(newIssuers, { [name]: kit.issuer }); + console.groupEnd(); + } + return mockWalletFactory( + { namesByAddressAdmin, zoe }, + { ...smartWalletIssuers, ...additionalIssuers }, + ); + }; let pid = 0; const runCoreEval = async ({ behavior, @@ -252,6 +270,9 @@ export const makeMockTools = async (t, bundleCache) => { return { makeQueryTool, + powers, + vatAdminState, + makeMockWalletFactory, installBundles: (bundleRoots, log) => installBundles(bundleCache, bundleRoots, installBundle, log), runCoreEval, From a77c256ab9a8f73d5e38d7973e3918d6fb375ea1 Mon Sep 17 00:00:00 2001 From: Thomas Greco <6646552+tgrecojs@users.noreply.github.com> Date: Fri, 13 Dec 2024 15:42:22 +0000 Subject: [PATCH 4/6] chore: added depositFacet.test.js file --- contract/Makefile | 21 +- contract/scripts/deploy-contract.js | 2 +- contract/src/airdrop.proposal.js | 12 +- .../tribbles-airdrop/depositFacet.test.js | 353 ++++++++++++++++++ ui/src/components/Orchestration/MakeOffer.tsx | 79 ++-- 5 files changed, 411 insertions(+), 56 deletions(-) create mode 100644 contract/test/tribbles-airdrop/depositFacet.test.js diff --git a/contract/Makefile b/contract/Makefile index f80c9a0..00ab430 100644 --- a/contract/Makefile +++ b/contract/Makefile @@ -1,5 +1,5 @@ CHAINID=agoriclocal -USER1ADDR=$(kagd keys show user1 -a --keyring-backend="test") +USER1ADDR=$(agd keys show tg -a --keyring-backend="test") ACCT_ADDR=$(USER1ADDR) BLD=000000ubld @@ -13,8 +13,8 @@ list: awk -v RS= -F: '$$1 ~ /^[^#%]+$$/ { print $$1 }' balance-q: - kagd keys show user1 -a --keyring-backend="test" - kagd query bank balances $(ACCT_ADDR) + agd keys show tg -a --keyring-backend="test" + agd query bank balances $(ACCT_ADDR) GAS_ADJUSTMENT=1.2 SIGN_BROADCAST_OPTS=--keyring-backend=test --chain-id=$(CHAINID) \ @@ -25,40 +25,40 @@ mint100: make FUNDS=1000$(ATOM) fund-acct cd /usr/src/agoric-sdk && \ yarn --silent agops vaults open --wantMinted 100 --giveCollateral 100 >/tmp/want-ist.json && \ - yarn --silent agops perf satisfaction --executeOffer /tmp/want-ist.json --from user1 --keyring-backend=test + yarn --silent agops perf satisfaction --executeOffer /tmp/want-ist.json --from tg --keyring-backend=test # Keep mint4k around a while for compatibility mint4k: make FUNDS=1000$(ATOM) fund-acct cd /usr/src/agoric-sdk && \ yarn --silent agops vaults open --wantMinted 4000 --giveCollateral 1000 >/tmp/want4k.json && \ - yarn --silent agops perf satisfaction --executeOffer /tmp/want4k.json --from user1 --keyring-backend=test + yarn --silent agops perf satisfaction --executeOffer /tmp/want4k.json --from tg --keyring-backend=test FUNDS=321$(BLD) fund-acct: - kagd tx bank send validator $(ACCT_ADDR) $(FUNDS) \ + agd tx bank send validator $(ACCT_ADDR) $(FUNDS) \ $(SIGN_BROADCAST_OPTS) \ -o json >,tx.json jq '{code: .code, height: .height}' ,tx.json gov-q: - kagd query gov proposals --output json | \ + agd query gov proposals --output json | \ jq -c '.proposals[] | [.proposal_id,.voting_end_time,.status]' gov-voting-q: - kagd query gov proposals --status=voting_period --output json | \ + agd query gov proposals --status=voting_period --output json | \ jq -c '.proposals[].proposal_id' PROPOSAL=1 VOTE_OPTION=yes vote: - kagd tx gov vote $(PROPOSAL) $(VOTE_OPTION) --from=validator \ + agd tx gov vote $(PROPOSAL) $(VOTE_OPTION) --from=validator \ $(SIGN_BROADCAST_OPTS) \ -o json >,tx.json jq '{code: .code, height: .height}' ,tx.json instance-q: - kagd query vstorage data published.agoricNames.instance -o json + agd query vstorage data published.agoricNames.instance -o json start-contract: check-contract-airdrop @@ -73,7 +73,6 @@ start: make start-contract-airdrop start-contract start-contract-airdrop: yarn node scripts/deploy-contract.js \ --install src/airdrop.contract.js \ - --eval src/platform-goals/board-aux.core.js \ --eval src/airdrop.proposal.js start-contract-swap: diff --git a/contract/scripts/deploy-contract.js b/contract/scripts/deploy-contract.js index 1f23ff2..7e6a2d7 100755 --- a/contract/scripts/deploy-contract.js +++ b/contract/scripts/deploy-contract.js @@ -15,7 +15,7 @@ const options = { help: { type: 'boolean' }, install: { type: 'string' }, eval: { type: 'string', multiple: true }, - service: { type: 'string', default: 'kagd' }, + service: { type: 'string', default: 'agd' }, workdir: { type: 'string', default: '/workspace/contract' }, }; /** diff --git a/contract/src/airdrop.proposal.js b/contract/src/airdrop.proposal.js index b058c5c..fdbced0 100644 --- a/contract/src/airdrop.proposal.js +++ b/contract/src/airdrop.proposal.js @@ -4,6 +4,7 @@ import { makeMarshal } from '@endo/marshal'; import { Fail } from '@endo/errors'; import { makeTracer, deeplyFulfilledObject } from '@agoric/internal'; import { makeStorageNodeChild } from '@agoric/internal/src/lib-chainStorage.js'; +import { fixHub } from './fixHub.js'; const AIRDROP_TIERS_STATIC = [9000n, 6500n, 3500n, 1500n, 750n].map( x => x * 1_000_000n, @@ -121,7 +122,6 @@ export const startAirdrop = async (powers, config = defaultConfig) => { const { consume: { namesByAddressAdmin, - namesByAddress, bankManager, board, chainTimerService, @@ -169,7 +169,7 @@ export const startAirdrop = async (powers, config = defaultConfig) => { 'can not start contract without merkleRoot???', ); trace('AFTER assert(config?.options?.merkleRoot'); - const marshaller = await E(board).getReadonlyMarshaller(); + const namesByAddress = await fixHub(namesByAddressAdmin); const startOpts = { installation: await airdropInstallationP, @@ -182,8 +182,7 @@ export const startAirdrop = async (powers, config = defaultConfig) => { privateArgs: await deeplyFulfilledObject( harden({ timer, - storageNode, - marshaller, + namesByAddress, }), ), }; @@ -297,14 +296,15 @@ export const permit = Object.values(airdropManifest)[0]; export const defaultProposalBuilder = async ({ publishRef, install }) => { return harden({ // Somewhat unorthodox, source the exports from this builder module - sourceSpec: '@agoric/builders/scripts/testing/start-tribbles-airdrop.js', + sourceSpec: + '/workspaces/dapp-ertp-airdrop/contract/src/airdrop.proposal.js', getManifestCall: [ 'getManifestForAirdrop', { installKeys: { tribblesAirdrop: publishRef( install( - '@agoric/orchestration/src/examples/airdrop/airdrop.contract.js', + '/workspaces/dapp-ertp-airdrop/contract/src/airdrop.contract.js', ), ), }, diff --git a/contract/test/tribbles-airdrop/depositFacet.test.js b/contract/test/tribbles-airdrop/depositFacet.test.js new file mode 100644 index 0000000..30d910d --- /dev/null +++ b/contract/test/tribbles-airdrop/depositFacet.test.js @@ -0,0 +1,353 @@ +/* eslint-disable import/order */ +// @ts-check +/* global setTimeout, fetch */ +// XXX what's the state-of-the-art in ava setup? +// eslint-disable-next-line import/order +import { test as anyTest } from '../prepare-test-env-ava.js'; + +import { createRequire } from 'module'; +import { env as ambientEnv } from 'node:process'; +import * as ambientChildProcess from 'node:child_process'; +import * as ambientFsp from 'node:fs/promises'; +import { E, passStyleOf } from '@endo/far'; +import { extract } from '@agoric/vats/src/core/utils.js'; +import { + makeTerms, + permit, + main, + startAirdrop, +} from '../../src/airdrop.local.proposal.js'; +import { + makeBundleCacheContext, + getBundleId, +} from '../../tools/bundle-tools.js'; +import { makeE2ETools } from '../../tools/e2e-tools.js'; +import { + makeNameProxy, + makeAgoricNames, +} from '../../tools/ui-kit-goals/name-service-client.js'; +import { makeMockTools, mockBootstrapPowers } from '../../tools/boot-tools.js'; +import { merkleTreeAPI } from '../../src/merkle-tree/index.js'; +import { makeStableFaucet } from '../mintStable.js'; +import { makeOfferArgs, simulateClaim } from './actors.js'; +import { oneDay } from '../../src/helpers/time.js'; +import { merkleTreeObj } from './generated_keys.js'; +import { AmountMath } from '@agoric/ertp'; + +const { accounts } = merkleTreeObj; +// import { makeAgdTools } from '../agd-tools.js'; + +/** @type {import('ava').TestFn>>} */ +const test = anyTest; + +const nodeRequire = createRequire(import.meta.url); + +const bundleRoots = { + tribblesAirdrop: nodeRequire.resolve('../../src/airdrop.contract.js'), +}; + +const scriptRoots = { + tribblesAirdrop: nodeRequire.resolve('../../src/airdrop.local.proposal.js'), +}; + +/** @param {import('ava').ExecutionContext} t */ +const makeTestContext = async t => { + const bc = await makeBundleCacheContext(t); + + const { E2E } = ambientEnv; + const { execFileSync, execFile } = ambientChildProcess; + const { writeFile } = ambientFsp; + + /** @type {import('../../tools/agd-lib.js').ExecSync} */ + const dockerExec = (file, args, opts = { encoding: 'utf-8' }) => { + const workdir = '/workspace/contract'; + const execArgs = ['compose', 'exec', '--workdir', workdir, 'agd']; + opts.verbose && + console.log('docker compose exec', JSON.stringify([file, ...args])); + return execFileSync('docker', [...execArgs, file, ...args], opts); + }; + + console.time('makeTestTools'); + console.timeLog('makeTestTools', 'start'); + // installBundles, + // runCoreEval, + // provisionSmartWallet, + // runPackageScript??? + const tools = await (E2E + ? makeE2ETools(t, bc.bundleCache, { + execFileSync: dockerExec, + execFile, + fetch, + setTimeout, + writeFile, + }) + : makeMockTools(t, bc.bundleCache)); + console.timeEnd('makeTestTools'); + + return { ...tools, ...bc }; +}; + +test.before(async t => (t.context = await makeTestContext(t))); + +// console.log('after makeAgdTools:::', { context: t.context }); + +test.serial('we1ll-known brand (ATOM) is available', async t => { + const { makeQueryTool } = t.context; + const hub0 = makeAgoricNames(makeQueryTool()); + const agoricNames = makeNameProxy(hub0); + await null; + const brand = { + ATOM: await agoricNames.brand.ATOM, + }; + t.log(brand); + t.is(passStyleOf(brand.ATOM), 'remotable'); +}); + +test.serial('install bundle: airdrop / tribblesAirdrop', async t => { + const { installBundles } = t.context; + console.time('installBundles'); + console.timeLog('installBundles', Object.keys(bundleRoots).length, 'todo'); + const bundles = await installBundles(bundleRoots, (...args) => + console.timeLog('installBundles', ...args), + ); + + console.timeEnd('installBundles'); + + const id = getBundleId(bundles.tribblesAirdrop); + const shortId = id.slice(0, 8); + t.log('bundleId', shortId); + t.is(id.length, 3 + 128, 'bundleID length'); + t.regex(id, /^b1-.../); + console.groupEnd(); + Object.assign(t.context.shared, { bundles }); + t.truthy( + t.context.shared.bundles.tribblesAirdrop, + 't.context.shared.bundles should contain a property "tribblesAirdrop"', + ); +}); +const containsSubstring = (substring, string) => + new RegExp(substring, 'i').test(string); + +test.serial('deploy contract with core eval: airdrop / airdrop', async t => { + const { runCoreEval } = t.context; + const { bundles } = t.context.shared; + const bundleID = getBundleId(bundles.tribblesAirdrop); + + t.deepEqual( + containsSubstring(bundles.tribblesAirdrop.endoZipBase64Sha512, bundleID), + true, + ); + const merkleRoot = merkleTreeAPI.generateMerkleRoot( + accounts.map(x => x.pubkey.key), + ); + + const { vatAdminState } = await mockBootstrapPowers(t.log); + + vatAdminState.installBundle(bundleID, bundles.tribblesAirdrop); + + console.log('inside deploy test::', bundleID); + // this runCoreEval does not work + const name = 'airdrop'; + const result = await runCoreEval({ + name, + behavior: main, + entryFile: scriptRoots.tribblesAirdrop, + config: { + options: { + customTerms: { + ...makeTerms(), + merkleRoot: merkleTreeObj.root, + }, + tribblesAirdrop: { bundleID }, + merkleRoot, + }, + }, + }); + + t.log(result.voting_end_time, '#', result.proposal_id, name); + t.like(result, { + content: { + '@type': '/agoric.swingset.CoreEvalProposal', + }, + status: 'PROPOSAL_STATUS_PASSED', + }); +}); + +// test.serial('checkBundle()', async t => { +// const { tribblesAirdrop } = t.context.shared.bundles; +// t.deepEqual(await checkBundle(tribblesAirdrop), ''); +// }); +const makeMakeOfferSpec = instance => (account, feeAmount) => ({ + id: `${account.address}-offer-${Date.now()}`, + invitationSpec: { + source: 'contract', + instance, + publicInvitationMaker: 'makeClaimTokensInvitation', + }, + proposal: { give: { Fee: feeAmount } }, + offerArgs: { ...makeOfferArgs(account) }, +}); +test.serial('E2E test', async t => { + const merkleRoot = merkleTreeAPI.generateMerkleRoot( + accounts.map(x => x.pubkey.key), + ); + const { bundleCache } = t.context; + + t.log('starting contract with merkleRoot:', merkleRoot); + // Is there a better way to obtain a reference to this bundle??? + // or is this just fine?? + const { tribblesAirdrop } = t.context.shared.bundles; + + const bundleID = getBundleId(tribblesAirdrop); + const { powers, vatAdminState, makeMockWalletFactory } = await makeMockTools( + t, + bundleCache, + ); + const { feeMintAccess, zoe, chainTimerService } = powers.consume; + + vatAdminState.installBundle(bundleID, tribblesAirdrop); + const airdropPowers = extract(permit, powers); + await startAirdrop(airdropPowers, { + merkleRoot: merkleTreeObj.root, + options: { + customTerms: { + ...makeTerms(), + merkleRoot: merkleTreeObj.root, + }, + tribblesAirdrop: { bundleID }, + merkleRoot: merkleTreeObj.root, + }, + }); + + /** @type {import('../../src/airdrop.local.proposal.js').AirdropSpace} */ + // @ts-expeimport { merkleTreeObj } from '@agoric/orchestration/src/examples/airdrop/generated_keys.js'; + const airdropSpace = powers; + const instance = await airdropSpace.instance.consume.tribblesAirdrop; + + const terms = await E(zoe).getTerms(instance); + const { issuers, brands } = terms; + + const walletFactory = makeMockWalletFactory({ + Tribbles: issuers.Tribbles, + Fee: issuers.Fee, + }); + + console.log('BRANDS::', brands); + const wallets = { + alice: await walletFactory.makeSmartWallet(accounts[4].address), + bob: await walletFactory.makeSmartWallet(accounts[2].address), + }; + const { faucet, mintBrandedPayment } = makeStableFaucet({ + bundleCache, + feeMintAccess, + zoe, + }); + + await Object.values(wallets).map(async wallet => { + const pmt = await mintBrandedPayment(10n); + console.log('payment::', pmt); + await E(wallet.deposit).receive(pmt); + }); + const makeOfferSpec = makeMakeOfferSpec(instance); + + // Now that we have the instance, resume testing as above. + + // TODO: update simulateClaim to construct claimArgs object. + // see makeOfferArgs function for reference. + + const feePurse = await faucet(5n * 1_000_000n); + // const claimAttempt = simulateClaim( + // t, + // zoe, + // instance, + // feePurse, + // merkleTreeObj.accounts[4], + // ); + // await t.throwsAsync( + // claimAttempt, + // { + // message: messagesObject.makeIllegalActionString(PREPARED), + // }, + // 'makeClaimInvitation() should throw an error stemming from the contract not being ready to accept offers.', + // ); + + await E(chainTimerService).advanceBy(oneDay * (oneDay / 2n)); + const makeFeeAmount = () => AmountMath.make(brands.Fee, 5n); + + /** + * @summary this creates an AsyncGenerator object that emits values in the code blocks that follow. + * + * @example the first value is recieved as a result of the following code + * + * await E(aliceUpdates).next() + * .then(res => { + * console.log('update res', res); + * return res; + * }); + */ + const aliceUpdates = E(wallets.alice.offers).executeOffer( + makeOfferSpec(accounts[4], makeFeeAmount()), + ); + + /* + * + * { + * value: { + * updated: 'offerStatus', + * status: { + * id: 'agoric19s266pak0llft2gcapn64x5aa37ysqnqzky46y-offer-1734092744506', + * invitationSpec: [Object], + * proposal: [Object], + * offerArgs: [Object] + * } + * }, + * done: false + * } + */ + /** + * @typedef {{value: { updated: string, status: { id: string, invitationSpec: import('../../tools/wallet-tools.js').InvitationSpec, proposal:Proposal, offerArgs: {key: string, proof: []}}}}} OfferResult + */ + + /** @returns {OfferResult} */ + await E(aliceUpdates) + .next() + .then(res => { + console.log('update res', res); + return res; + }); + + const tribblesWatcher = await E(wallets.alice.peek).purseUpdates( + brands.Tribbles, + ); + + let payout; + for await (const value of tribblesWatcher) { + console.log('update from smartWallet', value); // Process the value here + payout = value; + } + t.deepEqual( + AmountMath.isGTE(payout, AmountMath.make(brands.Tribbles, 0n)), + true, + ); + // await simulateClaim( + // t, + // zoe, + // instance, + // await faucet(5n * 1_000_000n), + // accounts[4], + // true, + // `Allocation for address ${accounts[4].address} has already been claimed.`, + // ); +}); + +// test.serial('agoricNames.instances has contract: airdrop', async t => { +// const { makeQueryTool } = t.context; +// const hub0 = makeAgoricNames(makeQueryTool()); + +// const agoricNames = makeNameProxy(hub0); +// console.log({ agoricNames }); +// await null; +// const instance = await E(agoricNames).lookup('instance', 'tribblesAirdrop'); +// t.is(passStyleOf(instance), 'remotable'); +// t.log(instance); +// }); diff --git a/ui/src/components/Orchestration/MakeOffer.tsx b/ui/src/components/Orchestration/MakeOffer.tsx index 82157df..bffc97f 100644 --- a/ui/src/components/Orchestration/MakeOffer.tsx +++ b/ui/src/components/Orchestration/MakeOffer.tsx @@ -1,13 +1,18 @@ import { AgoricWalletConnection } from '@agoric/react-components'; import { DynamicToastChild } from '../Tabs'; import { useContractStore } from '../../store/contract'; -import {pubkeys, agoricGenesisAccounts, getProof, merkleTreeAPI } from '../../airdrop-data/genesis.keys.js' -import {getInclusionProof} from '../../airdrop-data/kagdkeys.js' +import { + pubkeys, + agoricGenesisAccounts, + getProof, + merkleTreeAPI, +} from '../../airdrop-data/genesis.keys.js'; +import { getInclusionProof } from '../../airdrop-data/agdkeys.js'; const generateInt = x => () => Math.floor(Math.random() * (x + 1)); const createTestTier = generateInt(4); // ? -const currentAccount = ({address}) => agoricGenesisAccounts.filter(x => x.address === address); - +const currentAccount = ({ address }) => + agoricGenesisAccounts.filter(x => x.address === address); const makeMakeOfferArgs = (keys = []) => @@ -16,7 +21,6 @@ const makeMakeOfferArgs = proof: merkleTreeAPI.generateMerkleProof(key, keys), address, tier: createTestTier(), - }); const makeOfferArgs = makeMakeOfferArgs(pubkeys); @@ -28,8 +32,6 @@ export const makeOffer = async ( handleToggle: () => void, setStatusText: React.Dispatch>, ) => { - - const { instances, brands } = useContractStore.getState(); const instance = instances?.['tribblesAirdrop']; @@ -38,28 +40,28 @@ export const makeOffer = async ( handleToggle(); throw Error('No contract instance or brands found.'); } -const proof = [ + const proof = [ { hash: '149c44ac60f5c1da0029e1a4cb0a9c7a6a92ef49046a55d948992f46ba8c017f', - direction: 'right' + direction: 'right', }, { hash: '71d8de5c3dfbe37a00a512058cfb3a2d5ad17fca96156ec26bba7233cddb54f1', - direction: 'left' + direction: 'left', }, { hash: '1d572b148312772778d7c118bdc17770352d10c271b6c069f84b9796ff3ce514', - direction: 'left' + direction: 'left', }, { hash: 'aa9d927f3ecc8b316270a2901cc6a062fd47e863d8667af33ddd83d491b63e03', - direction: 'right' + direction: 'right', }, { hash: '6339fcd7509730b081b2e11eb382d88fe0c583eaec9a4d924e13e38553e9a5fa', - direction: 'left' - } - ] + direction: 'left', + }, + ]; // fetch the BLD brand const istBrand = brands.IST; if (!istBrand) { @@ -69,7 +71,7 @@ const proof = [ } const want = {}; - const give = { Fee: { brand: istBrand, value: 5n} }; + const give = { Fee: { brand: istBrand, value: 5n } }; const offerId = Date.now(); @@ -91,31 +93,32 @@ const proof = [ */ - const offerArgsValue = makeOfferArgs({ - address: wallet.address, - tier: createTestTier(), - ...getProof(wallet.address) - }); - - const STRING_CONSTANTS = { - OFFER_TYPES: { - AGORIC_CONTRACT: 'agoricContract' - }, - OFFER_NAME: 'makeClaimTokensInvitation', - INSTANCE: { - PATH: 'tribblesAirdrop' - }, - ISSUERS: { - TRIBBLES: 'Tribbles', - IST: 'IST', - BLD: 'BLD' - } - }; + const offerArgsValue = makeOfferArgs({ + address: wallet.address, + tier: createTestTier(), + ...getProof(wallet.address), + }); + const STRING_CONSTANTS = { + OFFER_TYPES: { + AGORIC_CONTRACT: 'agoricContract', + }, + OFFER_NAME: 'makeClaimTokensInvitation', + INSTANCE: { + PATH: 'tribblesAirdrop', + }, + ISSUERS: { + TRIBBLES: 'Tribbles', + IST: 'IST', + BLD: 'BLD', + }, + }; - console.log({offerArgsValue}, proof === offerArgsValue.proof, {current: currentAccount(wallet) }) + console.log({ offerArgsValue }, proof === offerArgsValue.proof, { + current: currentAccount(wallet), + }); - await wallet?.makeOffer( + await wallet?.makeOffer( { source: STRING_CONSTANTS.OFFER_TYPES.AGORIC_CONTRACT, instancePath: [STRING_CONSTANTS.INSTANCE.PATH], From 6ea4f424d2e6a108f489005e0fd9325fbf772a5f Mon Sep 17 00:00:00 2001 From: Thomas Greco <6646552+tgrecojs@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:11:05 +0000 Subject: [PATCH 5/6] test(depositFacet): added test code for verifying that offers are being handled correctly when processed through updated offerHandler code --- contract/src/helpers/adts.js | 59 ++++++- contract/test/tribbles-airdrop/actors.js | 3 +- .../tribbles-airdrop/depositFacet.test.js | 157 ++++++++++-------- 3 files changed, 145 insertions(+), 74 deletions(-) diff --git a/contract/src/helpers/adts.js b/contract/src/helpers/adts.js index 0d94b5a..6ea30a5 100644 --- a/contract/src/helpers/adts.js +++ b/contract/src/helpers/adts.js @@ -45,4 +45,61 @@ const Either = (() => { return { Right, Left, of, tryCatch, fromNullable, fromUndefined }; })(); -export { Either }; +const Observable = subscribe => ({ + // Subscribes to the observable + subscribe, + map: f => + Observable(observer => + subscribe({ + next: val => observer.next(f(val)), + error: err => observer.error(err), + complete: () => observer.complete(), + }), + ), + // Transforms the observable itself using a function that returns an observable + chain: f => + Observable(observer => + subscribe({ + next: val => f(val).subscribe(observer), + error: err => observer.error(err), + complete: () => observer.complete(), + }), + ), + // Combines two observables process to behave as one + concat: other => + Observable(observer => { + let completedFirst = false; + const completeFirst = () => { + completedFirst = true; + other.subscribe(observer); + }; + subscribe({ + next: val => observer.next(val), + error: err => observer.error(err), + complete: completeFirst, + }); + if (completedFirst) { + other.subscribe(observer); + } + }), +}); + +// Static method to create an observable from a single value +Observable.of = x => + Observable(observer => { + observer.next(x); + observer.complete(); + }); + +// Static method to create an observational from asynchronous computation +Observable.fromPromise = promise => + Observable(observer => { + promise + .then(val => { + observer.next(val); + observer.complete(); + }) + .catch(err => observer.error(err)); + }); + +export { Either, Observable }; diff --git a/contract/test/tribbles-airdrop/actors.js b/contract/test/tribbles-airdrop/actors.js index e99741f..fee83da 100644 --- a/contract/test/tribbles-airdrop/actors.js +++ b/contract/test/tribbles-airdrop/actors.js @@ -13,11 +13,12 @@ export const makeOfferArgs = ({ key: '', }, address = 'agoric12d3fault', + tier = createTestTier(), }) => ({ key: pubkey.key, proof: merkleTreeObj.constructProof(pubkey), address, - tier: createTestTier(), + tier, }); /** diff --git a/contract/test/tribbles-airdrop/depositFacet.test.js b/contract/test/tribbles-airdrop/depositFacet.test.js index 30d910d..65ec257 100644 --- a/contract/test/tribbles-airdrop/depositFacet.test.js +++ b/contract/test/tribbles-airdrop/depositFacet.test.js @@ -29,11 +29,16 @@ import { import { makeMockTools, mockBootstrapPowers } from '../../tools/boot-tools.js'; import { merkleTreeAPI } from '../../src/merkle-tree/index.js'; import { makeStableFaucet } from '../mintStable.js'; -import { makeOfferArgs, simulateClaim } from './actors.js'; -import { oneDay } from '../../src/helpers/time.js'; +import { makeOfferArgs } from './actors.js'; import { merkleTreeObj } from './generated_keys.js'; import { AmountMath } from '@agoric/ertp'; +import { Observable } from '../../src/helpers/adts.js'; +import { createStore } from '../../src/tribbles/utils.js'; +import { head } from '../../src/helpers/objectTools.js'; +const AIRDROP_TIERS_STATIC = [9000n, 6500n, 3500n, 1500n, 750n].map( + x => x * 1_000_000n, +); const { accounts } = merkleTreeObj; // import { makeAgdTools } from '../agd-tools.js'; @@ -173,12 +178,8 @@ test.serial('deploy contract with core eval: airdrop / airdrop', async t => { }); }); -// test.serial('checkBundle()', async t => { -// const { tribblesAirdrop } = t.context.shared.bundles; -// t.deepEqual(await checkBundle(tribblesAirdrop), ''); -// }); -const makeMakeOfferSpec = instance => (account, feeAmount) => ({ - id: `${account.address}-offer-${Date.now()}`, +const makeMakeOfferSpec = instance => (account, feeAmount, id) => ({ + id: `offer-${id}`, invitationSpec: { source: 'contract', instance, @@ -203,19 +204,17 @@ test.serial('E2E test', async t => { t, bundleCache, ); - const { feeMintAccess, zoe, chainTimerService } = powers.consume; + const { feeMintAccess, zoe } = powers.consume; vatAdminState.installBundle(bundleID, tribblesAirdrop); const airdropPowers = extract(permit, powers); await startAirdrop(airdropPowers, { - merkleRoot: merkleTreeObj.root, options: { customTerms: { ...makeTerms(), merkleRoot: merkleTreeObj.root, }, tribblesAirdrop: { bundleID }, - merkleRoot: merkleTreeObj.root, }, }); @@ -256,22 +255,7 @@ test.serial('E2E test', async t => { // see makeOfferArgs function for reference. const feePurse = await faucet(5n * 1_000_000n); - // const claimAttempt = simulateClaim( - // t, - // zoe, - // instance, - // feePurse, - // merkleTreeObj.accounts[4], - // ); - // await t.throwsAsync( - // claimAttempt, - // { - // message: messagesObject.makeIllegalActionString(PREPARED), - // }, - // 'makeClaimInvitation() should throw an error stemming from the contract not being ready to accept offers.', - // ); - - await E(chainTimerService).advanceBy(oneDay * (oneDay / 2n)); + const makeFeeAmount = () => AmountMath.make(brands.Fee, 5n); /** @@ -285,59 +269,88 @@ test.serial('E2E test', async t => { * return res; * }); */ - const aliceUpdates = E(wallets.alice.offers).executeOffer( - makeOfferSpec(accounts[4], makeFeeAmount()), - ); - /* - * - * { - * value: { - * updated: 'offerStatus', - * status: { - * id: 'agoric19s266pak0llft2gcapn64x5aa37ysqnqzky46y-offer-1734092744506', - * invitationSpec: [Object], - * proposal: [Object], - * offerArgs: [Object] - * } - * }, - * done: false - * } - */ + const [aliceTier] = [0]; + const [aliceUpdates, alicePurse] = [ + E(wallets.alice.offers).executeOffer( + makeOfferSpec({ ...accounts[4], tier: 0 }, makeFeeAmount(), 0), + ), + E(wallets.alice.peek).purseUpdates(brands.Tribbles), + ]; + /** * @typedef {{value: { updated: string, status: { id: string, invitationSpec: import('../../tools/wallet-tools.js').InvitationSpec, proposal:Proposal, offerArgs: {key: string, proof: []}}}}} OfferResult */ - /** @returns {OfferResult} */ - await E(aliceUpdates) - .next() - .then(res => { - console.log('update res', res); - return res; + const reducerFn = (state = [], action) => { + const { type, payload } = action; + switch (type) { + case 'NEW_RESULT': + return [...state, payload]; + default: + return state; + } + }; + const handleNewResult = result => ({ + type: 'NEW_RESULT', + payload: result.value, + }); + + const makeAsyncObserverObject = ( + generator, + completeMessage = 'Default completion message.', + maxCount = Infinity, + ) => + Observable(async observer => { + const iterator = E(generator); + const { dispatch, getStore } = createStore(reducerFn, []); + // eslint-disable-next-line no-constant-condition + while (true) { + // eslint-disable-next-line @jessie.js/safe-await-separator + const result = await iterator.next(); + if (result.done) { + console.log('result.done === true #### breaking loop'); + break; + } + dispatch(handleNewResult(result)); + if (getStore().length === maxCount) { + console.log('getStore().length === maxCoutn'); + break; + } + observer.next(result.value); + } + observer.complete({ message: completeMessage, values: getStore() }); }); - const tribblesWatcher = await E(wallets.alice.peek).purseUpdates( - brands.Tribbles, - ); + const traceFn = label => value => { + console.log(label, '::::', value); + return value; + }; - let payout; - for await (const value of tribblesWatcher) { - console.log('update from smartWallet', value); // Process the value here - payout = value; - } - t.deepEqual( - AmountMath.isGTE(payout, AmountMath.make(brands.Tribbles, 0n)), - true, - ); - // await simulateClaim( - // t, - // zoe, - // instance, - // await faucet(5n * 1_000_000n), - // accounts[4], - // true, - // `Allocation for address ${accounts[4].address} has already been claimed.`, - // ); + await makeAsyncObserverObject(aliceUpdates).subscribe({ + next: traceFn('SUBSCRIBE.NEXT'), + error: traceFn('AliceOffer Error'), + complete: ({ message, values }) => { + t.deepEqual(message, 'Default completion message.'); + t.deepEqual(values.length, 4); + }, + }); + + await makeAsyncObserverObject( + alicePurse, + 'Watch wallet completion', + 1, + ).subscribe({ + next: traceFn('TRIBBLES_WATCHER ### SUBSCRIBE.NEXT'), + error: traceFn('TRIBBLES_WATCHER #### SUBSCRIBE.ERROR'), + complete: ({ message, values }) => { + t.deepEqual(message, 'Watch wallet completion'); + t.deepEqual( + head(values), + AmountMath.make(brands.Tribbles, AIRDROP_TIERS_STATIC[aliceTier]), + ); + }, + }); }); // test.serial('agoricNames.instances has contract: airdrop', async t => { From 159c4c63f76efd003752e255e6fbfcd76afc3cc5 Mon Sep 17 00:00:00 2001 From: Thomas Greco <6646552+tgrecojs@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:17:03 -0500 Subject: [PATCH 6/6] test: added code further usage of , and demonstrates that its well-suitedness as generic interface for creating AsyncGenerator subscribtions (#150) This commit contains test for: - Handling bobs offer - Subscribing to bobs wallet - Verifying Alice's inability to make a second claim attempt --- .../tribbles-airdrop/depositFacet.test.js | 75 +++++++++++++++++-- 1 file changed, 67 insertions(+), 8 deletions(-) diff --git a/contract/test/tribbles-airdrop/depositFacet.test.js b/contract/test/tribbles-airdrop/depositFacet.test.js index 65ec257..55b0946 100644 --- a/contract/test/tribbles-airdrop/depositFacet.test.js +++ b/contract/test/tribbles-airdrop/depositFacet.test.js @@ -263,15 +263,15 @@ test.serial('E2E test', async t => { * * @example the first value is recieved as a result of the following code * - * await E(aliceUpdates).next() + * await E(alicesOfferUpdates).next() * .then(res => { * console.log('update res', res); * return res; * }); */ - const [aliceTier] = [0]; - const [aliceUpdates, alicePurse] = [ + const [aliceTier, bobTier] = [0, 2]; + const [alicesOfferUpdates, alicePurse] = [ E(wallets.alice.offers).executeOffer( makeOfferSpec({ ...accounts[4], tier: 0 }, makeFeeAmount(), 0), ), @@ -298,7 +298,7 @@ test.serial('E2E test', async t => { const makeAsyncObserverObject = ( generator, - completeMessage = 'Default completion message.', + completeMessage = 'Iterator lifecycle complete.', maxCount = Infinity, ) => Observable(async observer => { @@ -327,30 +327,89 @@ test.serial('E2E test', async t => { return value; }; - await makeAsyncObserverObject(aliceUpdates).subscribe({ + await makeAsyncObserverObject(alicesOfferUpdates).subscribe({ next: traceFn('SUBSCRIBE.NEXT'), error: traceFn('AliceOffer Error'), complete: ({ message, values }) => { - t.deepEqual(message, 'Default completion message.'); + t.deepEqual(message, 'Iterator lifecycle complete.'); t.deepEqual(values.length, 4); }, }); await makeAsyncObserverObject( alicePurse, - 'Watch wallet completion', + 'AsyncGenerator alicePurse has fufilled its requirements.', 1, ).subscribe({ next: traceFn('TRIBBLES_WATCHER ### SUBSCRIBE.NEXT'), error: traceFn('TRIBBLES_WATCHER #### SUBSCRIBE.ERROR'), complete: ({ message, values }) => { - t.deepEqual(message, 'Watch wallet completion'); + t.deepEqual( + message, + 'AsyncGenerator alicePurse has fufilled its requirements.', + ); t.deepEqual( head(values), AmountMath.make(brands.Tribbles, AIRDROP_TIERS_STATIC[aliceTier]), ); }, }); + const [alicesSecondClaim] = [ + E(wallets.alice.offers).executeOffer( + makeOfferSpec({ ...accounts[4], tier: 0 }, makeFeeAmount(), 0), + ), + ]; + + const alicesSecondOfferSubscriber = makeAsyncObserverObject( + alicesSecondClaim, + ).subscribe({ + next: traceFn('alicesSecondClaim ### SUBSCRIBE.NEXT'), + error: traceFn('alicesSecondClaim #### SUBSCRIBE.ERROR'), + complete: traceFn('alicesSecondClaim ### SUBSCRIBE.COMPLETE'), + }); + await t.throwsAsync(alicesSecondOfferSubscriber, { + message: `Allocation for address ${accounts[4].address} has already been claimed.`, + }); + const [bobsOfferUpdate, bobsPurse] = [ + E(wallets.bob.offers).executeOffer( + makeOfferSpec({ ...accounts[2], tier: bobTier }, makeFeeAmount(), 0), + ), + E(wallets.bob.peek).purseUpdates(brands.Tribbles), + ]; + + await makeAsyncObserverObject( + bobsOfferUpdate, + 'AsyncGenerator bobsOfferUpdate has fufilled its requirements.', + ).subscribe({ + next: traceFn('BOBS_OFFER_UPDATE:::: SUBSCRIBE.NEXT'), + error: traceFn('BOBS_OFFER_UPDATE:::: SUBSCRIBE.ERROR'), + complete: ({ message, values }) => { + t.deepEqual( + message, + 'AsyncGenerator bobsOfferUpdate has fufilled its requirements.', + ); + t.deepEqual(values.length, 4); + }, + }); + + await makeAsyncObserverObject( + bobsPurse, + 'AsyncGenerator bobsPurse has fufilled its requirements.', + 1, + ).subscribe({ + next: traceFn('TRIBBLES_WATCHER ### SUBSCRIBE.NEXT'), + error: traceFn('TRIBBLES_WATCHER #### SUBSCRIBE.ERROR'), + complete: ({ message, values }) => { + t.deepEqual( + message, + 'AsyncGenerator bobsPurse has fufilled its requirements.', + ); + t.deepEqual( + head(values), + AmountMath.make(brands.Tribbles, AIRDROP_TIERS_STATIC[bobTier]), + ); + }, + }); }); // test.serial('agoricNames.instances has contract: airdrop', async t => {