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": {} + } +} 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.contract.js b/contract/src/airdrop.contract.js index 8abd338..6106d32 100644 --- a/contract/src/airdrop.contract.js +++ b/contract/src/airdrop.contract.js @@ -5,24 +5,70 @@ 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 { makeTracer, mustMatch } from '@agoric/internal'; import { makeWaker, oneDay } from './helpers/time.js'; import { handleFirstIncarnation, makeCancelTokenMaker, } from './helpers/validation.js'; import { makeStateMachine } from './helpers/stateMachine.js'; -import { createClaimSuccessMsg } from './helpers/messages.js'; 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(), + key: M.string(), + proof: M.arrayOf(ProofDataShape), +}); + +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 +116,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 +131,7 @@ export const customTermsShape = harden({ startTime: M.bigint(), feeAmount: AmountShape, merkleRoot: M.string(), -}); +}; harden(customTermsShape); export const divideAmountByTwo = brand => amount => @@ -162,7 +207,21 @@ 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; + }; + /** @type {ContractTerms} */ const { startTime = 120n, @@ -344,21 +403,20 @@ export const start = async (zcf, privateArgs, baggage) => { * tier: number; * }} offerArgs */ - const claimHandler = (claimSeat, offerArgs) => { - const { - give: { Fee: claimTokensFee }, - } = claimSeat.getProposal(); - - const { proof, key: pubkey, address, tier } = offerArgs; + const claimHandler = async (claimSeat, offerArgs) => { + mustMatch( + offerArgs, + OfferArgsShape, + 'offerArgs does not contain the correct data.', + ); - // This line was added because of issues when testing - // Is there a way to gracefully test assertion failures???? - if (accountStore.has(pubkey)) { + if (accountStore.has(offerArgs.key)) { claimSeat.exit(); - throw new Error( - `Allocation for address ${address} has already been claimed.`, - ); + throw new Error(`Token allocation has already been claimed.`); } + const { proof, key: pubkey, tier } = offerArgs; + + const derivedAddress = computeAddress(pubkey); assert.equal( getMerkleRootFromMerkleProof(proof), @@ -366,27 +424,41 @@ export const start = async (zcf, privateArgs, baggage) => { 'Computed proof does not equal the correct root hash. ', ); - const paymentAmount = this.state.payoutArray[tier]; + const depositFacet = await getDepositFacet(derivedAddress); + const payment = await withdrawFromSeat(zcf, tokenHolderSeat, { + Tokens: this.state.payoutArray[tier], + }); + await Promise.all( + ...[ + Object.values(payment).map(pmtP => + E.when(pmtP, pmt => E(depositFacet).receive(pmt)), + ), + Promise.resolve( + accountStore.add(pubkey, { + address: derivedAddress, + pubkey, + tier, + amountAllocated: payment.value, + epoch: this.state.currentEpoch, + }), + ), + ], + ); rearrange( harden([ - [tokenHolderSeat, claimSeat, { Tokens: paymentAmount }], - [claimSeat, tokenHolderSeat, { Fee: claimTokensFee }], + [ + claimSeat, + tokenHolderSeat, + { Fee: claimSeat.getProposal().give.Fee }, + ], ]), ); claimSeat.exit(); - - accountStore.add(pubkey, { - address, - pubkey, - tier, - amountAllocated: paymentAmount, - epoch: this.state.currentEpoch, - }); - - return createClaimSuccessMsg(paymentAmount); + return 'makeClaimTokenInvitation success'; }; + return zcf.makeInvitation( claimHandler, messagesObject.makeClaimInvitationDescription(), diff --git a/contract/src/airdrop.local.proposal.js b/contract/src/airdrop.local.proposal.js index 3d2b777..88d81d3 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'; @@ -110,8 +111,8 @@ export const startAirdrop = async (powers, config) => { trace('powers.installation', powers.installation.consume[contractName]); const { consume: { - namesByAddressAdmin, - namesByAddress, + namesByAddressAdmin: namesByAddressAdminP, + // namesByAddress, // bankManager, board, chainTimerService, @@ -132,10 +133,11 @@ export const startAirdrop = async (powers, config) => { }, } = powers; - const [issuerIST, feeBrand, timer] = await Promise.all([ + const [issuerIST, feeBrand, timer, namesByAddressAdmin] = await Promise.all([ istIssuer, istBrand, chainTimerService, + namesByAddressAdminP, ]); const { customTerms } = config.options; @@ -152,6 +154,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 +171,7 @@ export const startAirdrop = async (powers, config) => { issuerNames: ['Tribbles'], privateArgs: harden({ timer, + namesByAddress, }), }; trace('BEFORE astartContract(permittedPowers, startOpts);', { startOpts }); diff --git a/contract/src/airdrop.proposal.js b/contract/src/airdrop.proposal.js index b058c5c..f234d7c 100644 --- a/contract/src/airdrop.proposal.js +++ b/contract/src/airdrop.proposal.js @@ -3,7 +3,7 @@ import { E } from '@endo/far'; 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, @@ -120,8 +120,7 @@ export const startAirdrop = async (powers, config = defaultConfig) => { trace('powers.installation', powers.installation.consume[contractName]); const { consume: { - namesByAddressAdmin, - namesByAddress, + namesByAddressAdmin: namesByAddressAdminP, bankManager, board, chainTimerService, @@ -145,11 +144,11 @@ export const startAirdrop = async (powers, config = defaultConfig) => { }, } = powers; - const [issuerIST, feeBrand, timer, storageNode] = await Promise.all([ + const [issuerIST, feeBrand, timer, namesByAddressAdmin] = await Promise.all([ istIssuer, istBrand, chainTimerService, - makeStorageNodeChild(chainStorage, contractName), + namesByAddressAdminP, ]); const { customTerms } = config.options; @@ -169,7 +168,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,11 +181,11 @@ export const startAirdrop = async (powers, config = defaultConfig) => { privateArgs: await deeplyFulfilledObject( harden({ timer, - storageNode, - marshaller, + namesByAddress, }), ), }; + trace('BEFORE astartContract(permittedPowers, startOpts);', { startOpts }); const { instance, creatorFacet } = await E(startUpgradable)(startOpts); @@ -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/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/src/helpers/adts.js b/contract/src/helpers/adts.js index 0d94b5a..ec6fdd6 100644 --- a/contract/src/helpers/adts.js +++ b/contract/src/helpers/adts.js @@ -45,4 +45,235 @@ 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)); + }); +const Reducer = run => ({ + run, + concat: other => Reducer((acc, x) => other.run(run(acc, x), x)), + contramap: f => Reducer((acc, x) => run(acc, f(x))), + map: f => Reducer((acc, x) => f(run(acc, x))), +}); + +const Id = x => ({ + map: f => Id(f(x)), + chain: f => f(x), + extract: () => x, + concat: o => Id(x.concat(o.extract())), +}); +Id.of = x => Id(x); + +const IdT = M => { + const Id = mx => ({ + map: f => Id(mx.map(f)), + chain: f => Id(mx.chain(x => f(x).extract())), + extract: () => mx, + }); + Id.of = x => Id(M.of(x)); + Id.lift = mx => Id(mx); + return Id; +}; + +const IO = run => ({ + run, + map: f => IO(() => f(run())), + chain: f => IO(() => f(run()).run()), + concat: other => IO(() => run().concat(other.run())), +}); +IO.of = x => IO(() => x); + +const Fn = g => ({ + map: f => Fn(x => f(g(x))), + chain: f => Fn(x => f(g(x)).run(x)), + concat: other => Fn(x => g(x).concat(other.run(x))), + run: g, +}); +Fn.ask = Fn(x => x); +Fn.of = x => Fn(() => x); + +const FnT = M => { + const Fn = g => ({ + map: f => Fn(x => g(x).map(f)), + chain: f => Fn(x => g(x).chain(y => f(y).run(x))), + concat: other => Fn(x => g(x).concat(other.run(x))), + run: g, + }); + Fn.ask = Fn(x => M.of(x)); + Fn.of = x => Fn(() => M.of(x)); + Fn.lift = x => Fn(() => x); + return Fn; +}; + +const EitherT = M => { + const Right = mx => ({ + isLeft: false, + extract: () => mx, + chain: f => Right(mx.chain(x => f(x).extract())), + map: f => Right(mx.map(f)), + fold: (_, g) => g(mx), + }); + + const Left = mx => ({ + isLeft: true, + extract: () => mx, + chain: _ => Left(mx), + map: _ => Left(mx), + fold: (h, _) => h(mx), + }); + + const of = x => Right(M.of(x)); + const tryCatch = f => { + try { + return Right(M.of(f())); + } catch (e) { + return Left(e); + } + }; + + const lift = Right; + + return { of, tryCatch, lift, Right, Left }; +}; + +const Task = fork => ({ + fork, + ap: other => + Task((rej, res) => fork(rej, f => other.fork(rej, x => res(f(x))))), + map: f => Task((rej, res) => fork(rej, x => res(f(x)))), + chain: f => Task((rej, res) => fork(rej, x => f(x).fork(rej, res))), + concat: other => + Task((rej, res) => fork(rej, x => other.fork(rej, y => res(x.concat(y))))), + fold: (f, g) => + Task((rej, res) => + fork( + x => f(x).fork(rej, res), + x => g(x).fork(rej, res), + ), + ), +}); +Task.of = x => Task((rej, res) => res(x)); +Task.rejected = x => Task((rej, res) => rej(x)); +Task.fromPromised = + fn => + (...args) => + Task((rej, res) => + fn(...args) + .then(res) + .catch(rej), + ); + +const TaskT = M => { + const Task = fork => ({ + fork, + map: f => Task((rej, res) => fork(rej, mx => res(mx.map(f)))), + chain: f => + Task((rej, res) => fork(rej, mx => mx.chain(x => f(x).fork(rej, res)))), + }); + Task.lift = x => Task((rej, res) => res(x)); + Task.of = x => Task((rej, res) => res(M.of(x))); + Task.rejected = x => Task((rej, res) => rej(x)); + + return Task; +}; + +const State = run => ({ + run, + chain: f => + State(x => { + const [y, s] = run(x); + return f(y).run(s); + }), + map: f => + State(x => { + const [y, s] = run(x); + return [f(y), s]; + }), + concat: other => + State(x => { + const [y, s] = run(x); + const [y1, _s1] = other.run(x); + return [y.concat(y1), s]; + }), +}); + +State.of = x => State(s => [x, s]); +State.get = State(x => [x, x]); +State.modify = f => State(s => [null, f(s)]); +State.put = x => State(s => [null, x]); + +const StateT = M => { + const State = run => ({ + run, + chain: f => State(x => run(x).chain(([y, s]) => f(y).run(s))), + map: f => State(x => run(x).map(([y, s]) => [f(y), s])), + concat: other => + State(x => + run(x).chain(([y, s]) => + other.run(x).map(([y1, s1]) => [y.concat(y1), s]), + ), + ), + }); + + State.lift = m => State(s => m.map(x => [x, s])); + State.of = x => State(s => M.of([x, s])); + State.get = State(x => M.of([x, x])); + State.modify = f => State(s => M.of([null, f(s)])); + State.put = x => State(s => M.of([null, x])); + + return State; +}; + +export { Either, Observable, Fn, FnT, EitherT, State, Task }; diff --git a/contract/src/tribbles/types.js b/contract/src/tribbles/types.js deleted file mode 100644 index fafb081..0000000 --- a/contract/src/tribbles/types.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @typedef {object} NatInstance - * Represents a natural number with semigroup concatenation capabilities. - * - * @property {import('@agoric/ertp/src/types.js').NatValue} value - The integer value of the natural number. - * @property {function(NatInstance): NatInstance} concat - A binary function - * that takes another NatInstance and returns the sum NatInstance holding the - * @property {function(): import('@agoric/ertp/src/types.js').NatValue} fold - A function that returns the integer - * value contained in the NatInstance. - * @property {function(): string} inspect - A function that returns a string representation of the NatInstance. - */ - -/** - * @typedef {object} EpochDetails - * @property {bigint} windowLength Length of epoch in seconds. This value is used by the contract's timerService to schedule a wake up that will fire once all of the seconds in an epoch have elapsed - * @property {import('@agoric/ertp/src/types.js').NatValue} tokenQuantity The total number of tokens recieved by each user who claims during a particular epoch. - * @property {bigint} index The index of a particular epoch. - * @property {number} inDays Length of epoch formatted in total number of days - */ - -/** - * Represents cosmos account information. - * @typedef {object} cosmosAccountInformation - * @property {string} prefix - The prefix. - * @property {object} pubkey - The public key. - * @property {string} pubkey.type - The type of the public key. - * @property {string} pubkey.value - The value of the public key. - * @property {string} expected - The expected value. - */ - -/** - * Object used for test purpoes only. The ExpectedValue - * @typedef {object} ExpectedValue - * @property {any} expected - */ - -/** - * Represents a testable account with cosmos account information and expected value. - * @typedef {cosmosAccountInformation & ExpectedValue} TestableAccount - */ - -/** - * Represents a testable account with cosmos account information and expected value. - * @typedef {cosmosAccountInformation & {tier: string}} EligibleAccountObject - */ - -/** - * @typedef {object} CustomContractTerms - * @property {bigint[]} initialPayoutValues Values to be used when constructing each amount that will be paid to claimants according to their tier. - * @property {import('@agoric/ertp/src/types.js').Amount} feePrice The fee associated with exercising one's right to claim a token. - * @property {bigint} targetTokenSupply Base supply of tokens to be distributed throughout an airdrop campaign. - * @property {string} tokenName Name of the token to be created and then airdropped to eligible claimaints. - * @property {number} targetNumberOfEpochs Total number of epochs the airdrop campaign will last for. - * @property {bigint} targetEpochLength Length of time for each epoch, denominated in seconds. - * @property {import('@agoric/time/src/types').RelativeTimeRecord} startTime Length of time (denoted in seconds) between the time in which the contract is started and the time at which users can begin claiming tokens. - * @property {string} merkleRoot Root hash of merkle tree containing all eligible claimans, represented as a hex string. - */ - -/** - * @typedef {object} DefaultZCFTerms - * @property {import('@agoric/ertp/src/types.js').Brand[]} brands - * @property {import('@agoric/ertp/src/types.js').Issuer[]} issuers - */ - -/** - * Represents a testable account with cosmos account information and expected value. - * @typedef {CustomContractTerms & DefaultZCFTerms} ContractTerms - */ diff --git a/contract/src/types.js b/contract/src/types.js index 893d3bd..1415498 100644 --- a/contract/src/types.js +++ b/contract/src/types.js @@ -13,3 +13,91 @@ * @param {Uint8Array} data - The input Uint8Array to hash. * @returns {string} The hexadecimal representation of the SHA-256 hash of the input data. */ + +/** + * @typedef {object} NatInstance + * Represents a natural number with semigroup concatenation capabilities. + * + * @property {import('@agoric/ertp/src/types.js').NatValue} value - The integer value of the natural number. + * @property {function(NatInstance): NatInstance} concat - A binary function + * that takes another NatInstance and returns the sum NatInstance holding the + * @property {function(): import('@agoric/ertp/src/types.js').NatValue} fold - A function that returns the integer + * value contained in the NatInstance. + * @property {function(): string} inspect - A function that returns a string representation of the NatInstance. + */ + +/** + * @typedef {object} EpochDetails + * @property {bigint} windowLength Length of epoch in seconds. This value is used by the contract's timerService to schedule a wake up that will fire once all of the seconds in an epoch have elapsed + * @property {import('@agoric/ertp/src/types.js').NatValue} tokenQuantity The total number of tokens recieved by each user who claims during a particular epoch. + * @property {bigint} index The index of a particular epoch. + * @property {number} inDays Length of epoch formatted in total number of days + */ + +/** + * Represents cosmos account information. + * @typedef {object} cosmosAccountInformation + * @property {string} prefix - The prefix. + * @property {object} pubkey - The public key. + * @property {string} pubkey.type - The type of the public key. + * @property {string} pubkey.value - The value of the public key. + * @property {string} expected - The expected value. + */ + +/** + * @typedef {object} PublicKey + * @property {string} type - The type of the public key. + * @property {string} key - The actual key. + */ + +/** + * @typedef {object} AccountDetails + * @property {string} name - The name of the account. + * @property {string} type - The type of the account. + * @property {string} address - The address of the account. + * @property {PublicKey} pubkey - The public key of the account. + * @property {string} mnemonic - The mnemonic of the account. + */ + +/** + * @typedef {AccountDetails[]} AccountsArray + */ + +/** + * Object used for test purpoes only. The ExpectedValue + * @typedef {object} ExpectedValue + * @property {any} expected + */ + +/** + * Represents a testable account with cosmos account information and expected value. + * @typedef {cosmosAccountInformation & ExpectedValue} TestableAccount + */ + +/** + * Represents a testable account with cosmos account information and expected value. + * @typedef {cosmosAccountInformation & {tier: string}} EligibleAccountObject + */ + +/** + * @typedef {object} CustomContractTerms + * @property {bigint[]} initialPayoutValues Values to be used when constructing each amount that will be paid to claimants according to their tier. + * @property {import('@agoric/ertp/src/types.js').Amount} feePrice The fee associated with exercising one's right to claim a token. + * @property {bigint} targetTokenSupply Base supply of tokens to be distributed throughout an airdrop campaign. + * @property {string} tokenName Name of the token to be created and then airdropped to eligible claimaints. + * @property {number} targetNumberOfEpochs Total number of epochs the airdrop campaign will last for. + * @property {bigint} targetEpochLength Length of time for each epoch, denominated in seconds. + * @property {import('@agoric/time/src/types.js').RelativeTimeRecord} startTime Length of time (denoted in seconds) between the time in which the contract is started and the time at which users can begin claiming tokens. + * @property {string} merkleRoot Root hash of merkle tree containing all eligible claimans, represented as a hex string. + */ + +/** + * @typedef {object} DefaultZCFTerms + * @property {import('@agoric/ertp/src/types.js').Brand[]} brands + * @property {import('@agoric/ertp/src/types.js').Issuer[]} issuers + */ + +/** + * Represents a testable account with cosmos account information and expected value. + * @typedef {CustomContractTerms & DefaultZCFTerms} ContractTerms + */ diff --git a/contract/test/market-actors.js b/contract/test/market-actors.js index 5fa78bc..ca5f917 100644 --- a/contract/test/market-actors.js +++ b/contract/test/market-actors.js @@ -10,7 +10,7 @@ import { import { seatLike } from '../tools/wallet-tools.js'; const { entries, fromEntries, keys } = Object; - +/** @import { Brand, Issuer } from '@agoric/ertp/src/types.js'; */ /** * @typedef {{ * brand: Record> & { timer: unknown } @@ -29,7 +29,7 @@ const { entries, fromEntries, keys } = Object; /** * @param {import('ava').ExecutionContext} t * @param {{ - * wallet: import('./wallet-tools.js').MockWallet; + * wallet: import('../tools/wallet-tools.js').MockWallet; * queryTool: Pick; * }} mine * @param {{ diff --git a/contract/test/tribbles-airdrop/actors.js b/contract/test/tribbles-airdrop/actors.js index e99741f..3bd0e88 100644 --- a/contract/test/tribbles-airdrop/actors.js +++ b/contract/test/tribbles-airdrop/actors.js @@ -1,94 +1,125 @@ import { E } from '@endo/far'; -import { AmountMath } from '@agoric/ertp'; import { accounts } from '../data/agd-keys.js'; import { merkleTreeObj } from './generated_keys.js'; +import { Fn, Observable } from '../../src/helpers/adts.js'; +import { createStore } from '../../src/tribbles/utils.js'; const generateInt = x => () => Math.floor(Math.random() * (x + 1)); const createTestTier = generateInt(4); // ? + +const makeClaimOfferArgs = account => + Fn(({ merkleTreeAPI }) => ({ + key: account.pubkey.key, + tier: account.tier, + proof: merkleTreeAPI.constructProof(account.pubkey), + })); + +const makePauseOfferSpec = (instance, offerId = 'default-offer-id') => ({ + offerId, + invitationSpec: { + source: 'purse', + instance, + description: 'pause contract', + }, + proposal: {}, +}); + +const makeMakeOfferSpec = instance => (account, feeAmount, id) => ({ + id: `offer-${id}`, + invitationSpec: { + source: 'contract', + instance, + publicInvitationMaker: 'makeClaimTokensInvitation', + }, + proposal: { give: { Fee: feeAmount } }, + offerArgs: { + key: account.pubkey.key, + proof: merkleTreeObj.constructProof(account.pubkey), + tier: account.tier, + }, +}); + const publicKeys = accounts.map(x => x.pubkey.key); +/** + * @param {import('../../src/types.js').AccountDetails} account + */ +const handleConstructClaimOffer = account => + makeClaimOfferArgs(account).chain(offerArgs => + Fn(({ makeFeeAmount, instance, invitationMaker }) => ({ + id: `offer-${account.address}`, + invitationSpec: { + source: 'contract', + instance, + publicInvitationMaker: invitationMaker, + }, + proposal: { give: { Fee: makeFeeAmount() } }, + offerArgs, + })), + ); export const makeOfferArgs = ({ pubkey = { key: '', }, - address = 'agoric12d3fault', + tier = createTestTier(), }) => ({ key: pubkey.key, proof: merkleTreeObj.constructProof(pubkey), - address, - tier: createTestTier(), + tier, }); -/** - * Eligible claimant exercises their right to claim tokens. - * - * @param {import('ava').ExecutionContext} t - * @param {ZoeService} zoe - * @param {import('@agoric/zoe/src/zoeService/utils').StartContractInstance} instance - * @param {import('@agoric/ertp/src/types').Purse} feePurse - * @param {{pubkey: {key: string, type: string}, address: string, tier?: number, name?: string, type?:string}} accountObject - * @param {boolean} shouldThrow boolean flag indicating whether or not the contract is expected to throw an error. - * @param {string} errorMessage Error message produced by contract resulting from some error arising during the claiming process. - * - */ -const simulateClaim = async ( - t, - zoe, - instance, - feePurse, - accountObject, - shouldThrow = false, - errorMessage = '', -) => { - const [pfFromZoe, terms] = await Promise.all([ - E(zoe).getPublicFacet(instance), - E(zoe).getTerms(instance), - ]); - - const { brands, issuers } = terms; - - const claimOfferArgs = makeOfferArgs(accountObject); - - console.log('TERMS:::', { terms, claimOfferArgs }); - - const proposal = { - give: { Fee: AmountMath.make(brands.Fee, 5n) }, - }; - t.log('Alice gives', proposal.give); - - const feePayment = await E(feePurse).withdraw( - AmountMath.make(brands.Fee, 5n), - ); - const [invitation, payoutValues] = await Promise.all([ - E(pfFromZoe).makeClaimTokensInvitation(), - E(pfFromZoe).getPayoutValues(), - ]); +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, +}); - if (!shouldThrow) { - const seat = await E(zoe).offer( - invitation, - proposal, - { Fee: feePayment }, - harden(claimOfferArgs), - ); - const airdropPayout = await E(seat).getPayout('Tokens'); +const makeAsyncObserverObject = ( + generator, + completeMessage = 'Iterator lifecycle complete.', + 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 actual = await E(issuers.Tribbles).getAmountOf(airdropPayout); - t.log('Alice payout brand', actual.brand); - t.log('Alice payout value', actual.value); - t.deepEqual(actual, payoutValues[claimOfferArgs.tier]); - } else { - const badSeat = E(zoe).offer( - invitation, - proposal, - { Fee: feePayment }, - harden(claimOfferArgs), - ); - await t.throwsAsync(E(badSeat).getOfferResult(), { - message: errorMessage, - }); - } +const traceFn = label => value => { + console.log(label, '::::', value); + return value; }; -export { simulateClaim }; +export { + createTestTier, + makeAsyncObserverObject, + handleConstructClaimOffer, + makeClaimOfferArgs, + makeMakeOfferSpec, + makePauseOfferSpec, + traceFn, +}; diff --git a/contract/test/tribbles-airdrop/contract.test.js b/contract/test/tribbles-airdrop/contract.test.js index 95667c4..805f414 100644 --- a/contract/test/tribbles-airdrop/contract.test.js +++ b/contract/test/tribbles-airdrop/contract.test.js @@ -1,326 +1,583 @@ -// @ts-check +/** + * @file contract.test.js + * @description this test file demonstrates behavior for all contract interaction. + */ -/* eslint-disable import/order -- https://github.com/endojs/endo/issues/1235 */ +/* 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 { E } from '@endo/far'; -import { makeNodeBundleCache } from '@endo/bundle-source/cache.js'; -import { makeZoeKitForTest } from '@agoric/zoe/tools/setup-zoe.js'; -import { AmountMath } from '@agoric/ertp'; - -import { makeStableFaucet } from '../mintStable.js'; -import buildManualTimer from '@agoric/zoe/tools/manualTimer.js'; -import { oneDay, TimeIntervals } from '../../src/helpers/time.js'; +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 { mockBootstrapPowers } from '../../tools/boot-tools.js'; -import { getBundleId } from '../../tools/bundle-tools.js'; -import { head } from '../../src/helpers/objectTools.js'; - -import { simulateClaim } from './actors.js'; -import { OPEN } from '../../src/airdrop.contract.js'; import { - startAirdrop, - permit, makeTerms, + permit, + main, + startAirdrop, } from '../../src/airdrop.local.proposal.js'; -import { makeFakeVatAdmin } from '@agoric/zoe/tools/fakeVatAdmin.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 } from './actors.js'; import { merkleTreeObj } from './generated_keys.js'; +import { AmountMath } from '@agoric/ertp'; +import { Observable, Task } from '../../src/helpers/adts.js'; +import { createStore } from '../../src/tribbles/utils.js'; +import { head } from '../../src/helpers/objectTools.js'; -const { accounts } = merkleTreeObj; -/** @typedef {typeof import('../../src/airdrop.contract.js').start} AssetContractFn */ +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 myRequire = createRequire(import.meta.url); -const contractPath = myRequire.resolve(`../../src/airdrop.contract.js`); -const AIRDROP_TIERS_STATIC = [9000n, 6500n, 3500n, 1500n, 750n]; +const makeAsyncObserverObject = ( + generator, + completeMessage = 'Iterator lifecycle complete.', + 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 traceFn = label => value => { + console.log(label, '::::', value); + return value; +}; + +const AIRDROP_TIERS_STATIC = [9000n, 6500n, 3500n, 1500n, 750n].map( + x => x * 1_000_000n, +); +const { accounts } = merkleTreeObj; +// import { makeAgdTools } from '../agd-tools.js'; /** @type {import('ava').TestFn>>} */ const test = anyTest; -const defaultCustomTerms = { - initialPayoutValues: AIRDROP_TIERS_STATIC, - targetNumberOfEpochs: 5, - targetEpochLength: TimeIntervals.SECONDS.ONE_DAY, - targetTokenSupply: 10_000_000n, - tokenName: 'Tribbles', - startTime: oneDay, - merkleRoot: merkleTreeObj.root, -}; - -const UNIT6 = 1_000_000n; +const nodeRequire = createRequire(import.meta.url); -const timerTracer = label => value => { - console.log(label, '::: latest #### ', value); - return value; +const bundleRoots = { + tribblesAirdrop: nodeRequire.resolve('../../src/airdrop.contract.js'), }; -const makeLocalTimer = async ( - createTimerFn = buildManualTimer(timerTracer('default timer'), 5n), -) => { - const timer = createTimerFn(); - const timerBrand = await E(timer).getTimerBrand(); - - return { - timer, - timerBrand, - }; +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); }; -/** - * Tests assume access to the zoe service and that contracts are bundled. - * - * See test-bundle-source.js for basic use of bundleSource(). - * Here we use a bundle cache to optimize running tests multiple times. - * - * @param {import('ava').TestFn} t - * - */ +/** @param {import('ava').ExecutionContext} t */ const makeTestContext = 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 testFeeIssuer = await E(zoe).getFeeIssuer(); - const testFeeBrand = await E(testFeeIssuer).getBrand(); - - const testFeeTokenFaucet = await makeStableFaucet({ - feeMintAccess, - zoe, - bundleCache, - }); - console.log('bundle:::', { bundle, bundleCache }); - return { - invitationIssuer, - zoe, - invitationIssuer, - bundle, - bundleCache, - makeLocalTimer, - testFeeTokenFaucet, - faucet: testFeeTokenFaucet.faucet, - testFeeBrand, - testFeeIssuer, - }; + const bc = await makeBundleCacheContext(t); + + 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))); -// IDEA: use test.serial and pass work products -// between tests using t.context. +// console.log('after makeAgdTools:::', { context: t.context }); -test('Install the contract', async t => { - const { zoe, bundle } = t.context; - - const installation = await E(zoe).install(bundle); - t.log(installation); - t.is(typeof installation, 'object'); +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'); }); -const startLocalInstance = async ( - t, - bundle, - { issuers: { Fee: feeIssuer }, zoe, terms: customTerms }, -) => { - const timer = buildManualTimer(); - - /** @type {ERef>} */ - const installation = await E(zoe).install(bundle); - - const { instance, publicFacet, creatorFacet } = await E(zoe).startInstance( - installation, - { Fee: feeIssuer }, - { - ...customTerms, - }, - { timer }, +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), ); - t.log('instance', { instance }); + 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"', + ); +}); - return { instance, installation, timer, publicFacet, creatorFacet }; -}; +const makeMakeOfferSpec = instance => (account, feeAmount, id) => ({ + id: `offer-${id}`, + invitationSpec: { + source: 'contract', + instance, + publicInvitationMaker: 'makeClaimTokensInvitation', + }, + proposal: { give: { Fee: feeAmount } }, + offerArgs: { ...makeOfferArgs(account) }, +}); -test.serial('Start the contract', async t => { - const { - zoe: zoeRef, - bundle, - bundleCache, - feeMintAccess, - testFeeBrand, - } = t.context; +test.serial('makeClaimTokensInvitation happy path::', async t => { + const merkleRoot = merkleTreeObj.root; + const { bundleCache } = t.context; - const testFeeIssuer = await E(zoeRef).getFeeIssuer(); + 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 testFeeTokenFaucet = await makeStableFaucet({ - feeMintAccess, - zoe: zoeRef, - bundleCache, - }); - console.log('context:', { testFeeTokenFaucet }); - - const localTestConfig = { - zoe: zoeRef, - issuers: { Fee: testFeeIssuer }, - terms: { - ...defaultCustomTerms, - feeAmount: AmountMath.make(testFeeBrand, 5n), - }, - }; + const bundleID = getBundleId(tribblesAirdrop); + const { powers, vatAdminState, makeMockWalletFactory, provisionSmartWallet } = + await makeMockTools(t, bundleCache); - const { instance } = await startLocalInstance(t, bundle, localTestConfig); - t.log(instance); - t.is(typeof instance, 'object'); -}); + const { feeMintAccess, zoe } = powers.consume; -test('Airdrop ::: happy paths', async t => { - const { zoe: zoeRef, bundle, faucet, testFeeBrand } = await t.context; - console.log(t.context); - const { instance, publicFacet, timer } = await startLocalInstance(t, bundle, { - zoe: zoeRef, - issuers: { Fee: await E(zoeRef).getFeeIssuer() }, - terms: { - ...defaultCustomTerms, - feeAmount: AmountMath.make(testFeeBrand, 5n), + vatAdminState.installBundle(bundleID, tribblesAirdrop); + const adminWallet = await provisionSmartWallet( + 'agoric1jng25adrtpl53eh50q7fch34e0vn4g72j6zcml', + { + BLD: 10n, }, - }); - - await E(timer).advanceBy(oneDay * (oneDay / 2n)); - - t.deepEqual(await E(publicFacet).getStatus(), OPEN); - - t.log({ faucet }); - await E(timer).advanceBy(oneDay); - const feePurse = await faucet(5n * UNIT6); - t.log(feePurse); - await t.deepEqual( - feePurse.getCurrentAmount(), - AmountMath.make(t.context.testFeeBrand, 5_000_000n), ); - await simulateClaim(t, zoeRef, instance, feePurse, head(accounts)); - - await E(timer).advanceBy(oneDay); + const zoeIssuer = await E(zoe).getInvitationIssuer(); - await simulateClaim(t, zoeRef, instance, feePurse, accounts[2]); + const zoeBrand = await zoeIssuer.getBrand(); + const adminZoePurse = E(adminWallet.peek).purseUpdates(zoeBrand); - await E(timer).advanceBy(oneDay); + const airdropPowers = extract(permit, powers); - t.deepEqual(await E(publicFacet).getStatus(), 'claim-window-open'); - - await E(timer).advanceBy(oneDay); -}); - -test.serial('delegate pause access :: makePauseContractInvitation', async t => { - const { - zoe: zoeRef, - invitationIssuer: zoeIssuer, - bundle, - faucet, - testFeeBrand, - } = await t.context; - console.log(t.context); - - const invitationPurse = await E(zoeIssuer).makeEmptyPurse(); - const depositOnlyFacet = invitationPurse.getDepositFacet(); - - const { instance, publicFacet, timer, creatorFacet } = - await startLocalInstance(t, bundle, { - zoe: zoeRef, - issuers: { Fee: await E(zoeRef).getFeeIssuer() }, - terms: { - ...defaultCustomTerms, - feeAmount: AmountMath.make(testFeeBrand, 5n), + await startAirdrop(airdropPowers, { + options: { + customTerms: { + ...makeTerms(), + merkleRoot: merkleTreeObj.root, }, - }); - - await E(creatorFacet).makePauseContractInvitation(depositOnlyFacet); - - const pauseInvitationAmt = invitationPurse.getCurrentAmount(); - - t.deepEqual( - pauseInvitationAmt.brand, - await E(zoeIssuer).getBrand(), - 'makePauseContractInvitation given a valid depositFacet should deposit an invitation into its purse.', - ); - - const pauseOffersPayment = invitationPurse.withdraw(pauseInvitationAmt); + tribblesAirdrop: { bundleID }, + }, + }); - // Claming is not yet active in contract. - // Code below produces: "Illegal state transition. Can not transition from state: prepared to state paused." - // await E(zoeRef).offer(pauseOffersPayment, undefined, undefined); + await makeAsyncObserverObject( + adminZoePurse, + 'invitation recieved', + 1, + ).subscribe({ + next: traceFn('ADMIN_WALLET::: NEXT'), + error: traceFn('ADMIN WALLET::: ERROR'), + complete: async ({ message, values }) => { + const [pauseInvitationDetails] = values; + t.deepEqual(message, 'invitation recieved'); + t.deepEqual(pauseInvitationDetails.brand, zoeBrand); + t.deepEqual( + head(pauseInvitationDetails.value).description, + 'pause contract', + ); + }, + }); + /** @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; - await E(timer).advanceBy(oneDay * (oneDay / 2n)); + const terms = await E(zoe).getTerms(instance); - t.deepEqual(await E(publicFacet).getStatus(), OPEN); + const { issuers, brands } = terms; - await E(timer).advanceBy(oneDay); - const feePurse = await faucet(5n * UNIT6); + const walletFactory = makeMockWalletFactory({ + Tribbles: issuers.Tribbles, + Fee: issuers.Fee, + }); - await simulateClaim(t, zoeRef, instance, feePurse, accounts[2]); + const wallets = { + alice: await walletFactory.makeSmartWallet(accounts[4].address), + bob: await walletFactory.makeSmartWallet(accounts[2].address), + }; + const { faucet, mintBrandedPayment } = makeStableFaucet({ + bundleCache, + feeMintAccess, + zoe, + }); - const adminSeat = await E(zoeRef).offer( - pauseOffersPayment, - undefined, - undefined, - ); + 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); + + await faucet(5n * 1_000_000n); + + const makeFeeAmount = () => AmountMath.make(brands.Fee, 5n); + + const [aliceTier, bobTier] = [0, 2]; + const [alice, bob] = [ + [ + E(wallets.alice.offers).executeOffer( + makeOfferSpec({ ...accounts[4], tier: 0 }, makeFeeAmount(), 0), + ), + E(wallets.alice.peek).purseUpdates(brands.Tribbles), + ], + [ + E(wallets.bob.offers).executeOffer( + makeOfferSpec({ ...accounts[2], tier: bobTier }, makeFeeAmount(), 0), + ), + E(wallets.bob.peek).purseUpdates(brands.Tribbles), + ], + ]; + + const [alicesOfferUpdates, alicesPurse] = alice; + const [bobsOfferUpdate, bobsPurse] = bob; + /** + * @typedef {{value: { updated: string, status: { id: string, invitationSpec: import('../../tools/wallet-tools.js').InvitationSpec, proposal:Proposal, offerArgs: {key: string, proof: []}}}}} OfferResult + */ + + await makeAsyncObserverObject(alicesOfferUpdates).subscribe({ + next: traceFn('SUBSCRIBE.NEXT'), + error: traceFn('AliceOffer Error'), + complete: ({ message, values }) => { + t.deepEqual(message, 'Iterator lifecycle complete.'); + t.deepEqual(values.length, 4); + }, + }); - t.deepEqual( - await adminSeat.hasExited(), - true, - 'adminSeat.hasExited() should return true following a succesful offer to pause the contract.', - ); + await makeAsyncObserverObject( + alicesPurse, + '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, + 'AsyncGenerator alicePurse has fufilled its requirements.', + ); + t.deepEqual( + head(values), + AmountMath.make(brands.Tribbles, AIRDROP_TIERS_STATIC[aliceTier]), + ); + }, + }); - await E(timer).advanceBy(oneDay); + 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 E(timer).advanceBy(oneDay); + await t.throwsAsync(alicesSecondOfferSubscriber, { + message: 'Token allocation has already been claimed.', + }); - t.deepEqual(await E(publicFacet).getStatus(), 'paused'); + 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); + }, + }); - // TODO: Validate that an offer make to contract fails when offer filter is present + 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( - 'MN-2 Task: Add a deployment test that exercises the core-eval that will be used to install & start the contract on chain.', + 'makeClaimTokensInvitation:: after executing makePauseContractInvitation', async t => { - const { bundle, testFeeBrand } = t.context; + const merkleRoot = merkleTreeAPI.generateMerkleRoot( + accounts.map(x => x.pubkey.key), + ); + const { bundleCache } = t.context; - const bundleID = getBundleId(bundle); - const { powers, vatAdminState } = await mockBootstrapPowers(t.log); + 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, + provisionSmartWallet, + } = await makeMockTools(t, bundleCache); const { feeMintAccess, zoe } = powers.consume; - // When the BLD staker governance proposal passes, - // the startup function gets called. - vatAdminState.installBundle(bundleID, bundle); + vatAdminState.installBundle(bundleID, tribblesAirdrop); + const adminWallet = await provisionSmartWallet( + 'agoric1jng25adrtpl53eh50q7fch34e0vn4g72j6zcml', + { + BLD: 10n, + }, + ); + + const zoeIssuer = await E(zoe).getInvitationIssuer(); + + const zoeBrand = await zoeIssuer.getBrand(); + const adminZoePurse = E(adminWallet.peek).purseUpdates(zoeBrand); + const airdropPowers = extract(permit, powers); + await startAirdrop(airdropPowers, { - merkleRoot: merkleTreeObj.root, options: { customTerms: { ...makeTerms(), merkleRoot: merkleTreeObj.root, }, tribblesAirdrop: { bundleID }, - merkleRoot: merkleTreeObj.root, }, }); - const sellSpace = powers; - const instance = await sellSpace.instance.consume.tribblesAirdrop; - console.log({ powers }); - // Now that we have the instance, resume testing as above. - const { bundleCache } = t.context; - const { faucet } = makeStableFaucet({ bundleCache, feeMintAccess, zoe }); + await makeAsyncObserverObject( + adminZoePurse, + 'invitation recieved', + 1, + ).subscribe({ + next: traceFn('ADMIN_WALLET::: NEXT'), + error: traceFn('ADMIN WALLET::: ERROR'), + complete: async ({ message, values }) => { + const [pauseInvitationDetails] = values; + t.deepEqual(message, 'invitation recieved'); + t.deepEqual(pauseInvitationDetails.brand, zoeBrand); + t.deepEqual( + head(pauseInvitationDetails.value).description, + 'pause contract', + ); + }, + }); + /** @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; - await simulateClaim( - t, + const terms = await E(zoe).getTerms(instance); + const { issuers, brands } = terms; + + const walletFactory = makeMockWalletFactory({ + Tribbles: issuers.Tribbles, + Fee: issuers.Fee, + }); + + const wallets = { + alice: await walletFactory.makeSmartWallet(accounts[4].address), + bob: await walletFactory.makeSmartWallet(accounts[2].address), + }; + const { faucet, mintBrandedPayment } = makeStableFaucet({ + bundleCache, + feeMintAccess, zoe, - instance, - await faucet(5n * UNIT6), - accounts[3], - ); + }); + + 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); + + await faucet(5n * 1_000_000n); + + const makeFeeAmount = () => AmountMath.make(brands.Fee, 5n); + + const pauseOffer = { + id: 'admin-pause-1', + invitationSpec: { + source: 'purse', + instance, + description: 'pause contract', + }, + proposal: {}, + }; + + const pauseOfferUpdater = E(adminWallet.offers).executeOffer(pauseOffer); + + await makeAsyncObserverObject(pauseOfferUpdater).subscribe({ + next: traceFn('pauseOfferUpdater ## next'), + error: traceFn('pauseOfferUpdater## Error'), + complete: traceFn('pauseOfferUpdater ## complete'), + }); + const [aliceTier, bobTier] = [0, 2]; + const [alice, bob] = [ + [ + E(wallets.alice.offers).executeOffer( + makeOfferSpec( + { ...accounts[4], tier: aliceTier }, + makeFeeAmount(), + 0, + ), + ), + E(wallets.alice.peek).purseUpdates(brands.Tribbles), + ], + [ + E(wallets.bob.offers).executeOffer( + makeOfferSpec({ ...accounts[2], tier: bobTier }, makeFeeAmount(), 0), + ), + E(wallets.bob.peek).purseUpdates(brands.Tribbles), + ], + ]; + + const [alicesOfferUpdates, alicesPurse] = alice; + const [bobsOfferUpdate, bobsPurse] = bob; + /** + * @typedef {{value: { updated: string, status: { id: string, invitationSpec: import('../../tools/wallet-tools.js').InvitationSpec, proposal:Proposal, offerArgs: {key: string, proof: []}}}}} OfferResult + */ + + t.throwsAsync(E(alicesOfferUpdates).next(), { + message: 'Airdrop can not be claimed when contract status is: paused.', + }); + + await makeAsyncObserverObject( + alicesPurse, + 'alicePurse after attempting to claim while paused should contain 0n tokens.', + 1, + ).subscribe({ + next: traceFn('TRIBBLES_WATCHER ### SUBSCRIBE.NEXT'), + error: traceFn('TRIBBLES_WATCHER #### SUBSCRIBE.ERROR'), + complete: ({ message, values }) => { + t.deepEqual( + message, + 'alicePurse after attempting to claim while paused should contain 0n tokens.', + ); + t.deepEqual(head(values), AmountMath.make(brands.Tribbles, 0n)); + }, + }); + 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: 'Airdrop can not be claimed when contract status is: paused.', + }); + + t.throwsAsync(E(bobsOfferUpdate).next(), { + message: 'Airdrop can not be claimed when contract status is: paused.', + }); + + await makeAsyncObserverObject( + bobsPurse, + 'bobsPurse after attempting to claim while paused should contain 0n tokens.', + 1, + ).subscribe({ + next: traceFn('TRIBBLES_WATCHER ### SUBSCRIBE.NEXT'), + error: traceFn('TRIBBLES_WATCHER #### SUBSCRIBE.ERROR'), + complete: ({ message, values }) => { + t.deepEqual( + message, + 'bobsPurse after attempting to claim while paused should contain 0n tokens.', + ); + t.deepEqual(head(values), AmountMath.make(brands.Tribbles, 0n)); + }, + }); }, ); diff --git a/contract/test/tribbles-airdrop/coreEval.test.js b/contract/test/tribbles-airdrop/coreEval.test.js new file mode 100644 index 0000000..3fed35d --- /dev/null +++ b/contract/test/tribbles-airdrop/coreEval.test.js @@ -0,0 +1,342 @@ +/** + * @file coreEval.test.js + * @description this file is meant to demonstrate that the coreEval process works as expected. + */ + +/* eslint-disable import/order */ +// @ts-check +/* global setTimeout, fetch */ +// 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 } from '../../tools/boot-tools.js'; +import { merkleTreeAPI } from '../../src/merkle-tree/index.js'; +import { makeStableFaucet } from '../mintStable.js'; +import { makeAsyncObserverObject, makeMakeOfferSpec } from './actors.js'; +import { merkleTreeObj } from './generated_keys.js'; +import { AmountMath } from '@agoric/ertp'; +import '../types.js'; +import { head } from '../../src/helpers/objectTools.js'; + +const traceFn = label => value => { + console.log(label, '::::', value); + return value; +}; + +const AIRDROP_TIERS_STATIC = [9000n, 6500n, 3500n, 1500n, 750n].map( + x => x * 1_000_000n, +); + +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('well-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( + 'runCoreEval test ::: 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), + ); + console.log('inside deploy test::', bundleID); + // this runCoreEval does not work + const name = 'tribblesAirdrop'; + 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( + 'makeClaimTokensInvitation:: after installing contract with coreEval', + 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 } = powers.consume; + + vatAdminState.installBundle(bundleID, tribblesAirdrop); + const airdropPowers = extract(permit, powers); + await startAirdrop(airdropPowers, { + options: { + customTerms: { + ...makeTerms(), + merkleRoot, + }, + tribblesAirdrop: { bundleID }, + }, + }); + + /** @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, + }); + + 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); + + await faucet(5n * 1_000_000n); + + const makeFeeAmount = () => AmountMath.make(brands.Fee, 5n); + + const [aliceTier, bobTier] = [0, 2]; + const [alicesOfferUpdates, alicePurse] = [ + E(wallets.alice.offers).executeOffer( + makeOfferSpec({ ...accounts[4], tier: 0 }, makeFeeAmount(), 0), + ), + E(wallets.alice.peek).purseUpdates(brands.Tribbles), + ]; + + await makeAsyncObserverObject(alicesOfferUpdates).subscribe({ + next: traceFn('SUBSCRIBE.NEXT'), + error: traceFn('AliceOffer Error'), + complete: ({ message, values }) => { + t.deepEqual(message, 'Iterator lifecycle complete.'); + t.deepEqual(values.length, 4); + }, + }); + + await makeAsyncObserverObject( + alicePurse, + '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, + 'AsyncGenerator alicePurse has fufilled its requirements.', + ); + t.deepEqual( + head(values), + AmountMath.make(brands.Tribbles, AIRDROP_TIERS_STATIC[aliceTier]), + ); + }, + }); + + 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 => { + 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/contract/test/tribbles-airdrop/e2e.test.js b/contract/test/tribbles-airdrop/e2e.test.js deleted file mode 100644 index dcf251f..0000000 --- a/contract/test/tribbles-airdrop/e2e.test.js +++ /dev/null @@ -1,267 +0,0 @@ -/* 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 { simulateClaim } from './actors.js'; -import { oneDay } from '../../src/helpers/time.js'; -import { merkleTreeObj } from './generated_keys.js'; - -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), ''); -// }); - -test.serial('E2E test', async t => { - const merkleRoot = merkleTreeAPI.generateMerkleRoot( - accounts.map(x => x.pubkey.key), - ); - - 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 } = await mockBootstrapPowers(t.log); - 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; - - // 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. - - 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)); - - await simulateClaim( - t, - zoe, - instance, - await faucet(5n * 1_000_000n), - accounts[4], - ); - - 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/contract/test/tribbles-airdrop/support.js b/contract/test/tribbles-airdrop/support.js index 1d6bef2..ba09bf4 100644 --- a/contract/test/tribbles-airdrop/support.js +++ b/contract/test/tribbles-airdrop/support.js @@ -50,7 +50,7 @@ const commonSetup = async t => { execFileFn: childProcess.execFile, }); const keyring = await makeKeyring(tools); - const deployBuilder = makeDeployBuilder(tools, fse.readJSON, execa); + const deployBuilder = await makeDeployBuilder(tools, fse.readJSON, execa); const retryUntilCondition = makeRetryUntilCondition({ log: t.log }); const startContract = async (contractName = '', contractBuilder = '') => { const { vstorageClient } = tools; diff --git a/contract/test/tribbles-airdrop/tools/e2e-tools.js b/contract/test/tribbles-airdrop/tools/e2e-tools.js index d2810b7..04cce6c 100644 --- a/contract/test/tribbles-airdrop/tools/e2e-tools.js +++ b/contract/test/tribbles-airdrop/tools/e2e-tools.js @@ -191,6 +191,7 @@ export const provisionSmartWallet = async ( /** @param {import('@agoric/smart-wallet/src/smartWallet.js').BridgeAction} bridgeAction */ const sendAction = async bridgeAction => { + console.log('inside sendAction::::', bridgeAction); // eslint-disable-next-line no-undef const capData = q.toCapData(harden(bridgeAction)); const offerBody = JSON.stringify(capData); @@ -203,6 +204,8 @@ export const provisionSmartWallet = async ( /** @param {import('@agoric/smart-wallet/src/offers.js').OfferSpec} offer */ async function* executeOffer(offer) { + console.log('------------------------'); + console.log('offer::', offer); const updates = q.follow(`published.wallet.${address}`, { delay }); const txInfo = await sendAction({ method: 'executeOffer', offer }); console.debug('spendAction', txInfo); @@ -263,6 +266,11 @@ export const provisionSmartWallet = async ( } async function* purseUpdates(brand) { + console.log('------------------------'); + console.log('purseUpdates##########::'); + + console.log('------------------------'); + console.log('brand::', brand); const brandAssetInfo = Object.values(byName).find(a => a.brand === brand); await null; if (brandAssetInfo) { diff --git a/contract/test/types.js b/contract/test/types.js new file mode 100644 index 0000000..3b40a28 --- /dev/null +++ b/contract/test/types.js @@ -0,0 +1,9 @@ +/** @import { Brand, Issuer } from '@agoric/ertp/src/types.js'; */ +/** + * @typedef {{ + * brand: Record> & { timer: unknown } + * issuer: Record> + * instance: Record> + * installation: Record> + * }} WellKnown + */ diff --git a/contract/test/wallet-kit-agd.js b/contract/test/wallet-kit-agd.js index 0eafcf1..1f32b27 100644 --- a/contract/test/wallet-kit-agd.js +++ b/contract/test/wallet-kit-agd.js @@ -78,11 +78,17 @@ export const makeWalletKit = (addr, opts) => { // eslint-disable-next-line no-unused-vars const sendOffer = async (offer, fee = 'auto') => { const actionj = formatAction(offer); + console.log('------------------------'); + console.log('inside send offer::'); + console.log('------------------------'); + console.log('actionj::', actionj); const trx = await agd.tx( ['swingset', 'wallet-action', actionj, '--allow-spend', '--trace'], { chainId, from: addr, yes: true }, ); - // console.log('code', trx.code); + console.log('------------------------'); + console.log('trx::', trx); + if (trx.code !== 0) throw Error(trx.rawlog); return trx; }; 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, diff --git a/contract/tsconfig.json b/contract/tsconfig.json index e9598fd..f8e7f2f 100644 --- a/contract/tsconfig.json +++ b/contract/tsconfig.json @@ -12,7 +12,8 @@ "downlevelIteration": true, "strictNullChecks": true, "noImplicitThis": true, - "noEmit": true + "noEmit": true, + "typeRoots": ["./src/types.js", "./test/types.js"] }, "include": [ "*.js", 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],