diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..04085c9 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +e2e: build build + yarn workspace dapp-agoric-orca-contract test; yarn workspace dapp-agoric-orca-contract build; yarn workspace dapp-agoric-orca-contract e2e +build: + yarn workspace dapp-agoric-orca-contract test; yarn workspace dapp-agoric-orca-contract build; +redeploy: + yarn workspace dapp-agoric-orca-contract deployc +test-orca: + yarn workspace dapp-agoric-orca-contract test +fund: + yarn workspace dapp-agoric-orca-contract fund +add-address: + yarn workspace dapp-agoric-orca-contract add:address +lint: + yarn workspace dapp-agoric-orca-contract lint diff --git a/README.md b/README.md index 7a544d4..685e19d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,148 @@ -# Simple Agoric ORChAstration Template +# Agoric Orchestration Basics Dapp - + ## Overview -Lorem +The Orchestration Basics dApp showcases various features of the orchestration API running inside of an end-to-end environment, and a user interface: + + +# Interface +you can run `yarn dev` inside of the `ui` folder. + + + + +# Setting up the local environment +See `agoric-sdk/multichain-testing/README.md` for more setup instructions + +you can run , run `agd status` to check if this was successful. If not try `make port-forward` again. + +Once this is running, you need to also run `make override-chain-registry`. This will update vstorage to work with the local startship environment. + +From `agoric-sdk/multichain-testing`, you can use this command to restart your environment for any reason. +``` +make teardown ; make stop; make stop-forward; make clean; make; make port-forward +``` + +## Multichain-testing Makefile Helpers +You can add these commands to the bottom of the `multichain-testing` `Makefile` for now: +```Makefile + +teardown: stop-forward stop clean delete + +corepack-setup: + corepack prepare yarn@4 --activate +corepack-enable: + corepack enable +test: + yarn test test/install-contracts.test.ts + +all: setup install + sleep 3 + make port-forward + sleep 120 + make fund-provision-pool + sleep 10 + make add-address + echo "done running" + +hermes-update: + kubectl exec -i hermes-agoric-osmosis-0 -c relayer -- hermes update client --host-chain agoriclocal --client 07-tendermint-1 + sleep 60 + make hermes-update +``` + + + +# Add a new address to the keychain inside of the kubernetes pod (for building/deploying inside of the pod) +From top level directory: +``` +make add-address +``` +paste address in the `Makefile` for `ADDR`. + +# Fund the account +This will fund the pool, provision the smart wallet, and will also fund `CLIENTADDR` and `CLIENT_OSMO_ADDR`. `CLIENTADDR` is your address from your browser wallet that you will use to interact with the orchestration dapp. `CLIENT_OSMO_ADDR` is the same, but your osmosis account. + +This can be ran from the top-level directory +``` +make fund +``` + +# Build & Deploy the dapp +From the top level directory, run: +``` +make +``` + +# Tests +From top-level directory: +``` +make test-orca +``` + +# tests from root directory +``` +yarn cache clean; yarn; yarn workspace dapp-agoric-orca-contract test ; rm -rf -v yarn.lock package-lock.json node_modules contract/node_modules; yarn; yarn workspace dapp-agoric-orca-contract test +``` + +without clean: +``` +yarn workspace dapp-agoric-orca-contract deploy +``` + +# deploy from root directory +``` +yarn cache clean; yarn; yarn workspace dapp-agoric-orca-contract test ; rm -rf -v yarn.lock package-lock.json node_modules contract/node_modules; yarn; yarn workspace dapp-agoric-orca contract:deploy +``` + +without clean: +``` +yarn workspace dapp-agoric-orca-contract deploy +``` + +# e2e build/deploy +``` +yarn workspace dapp-agoric-orca-contract deployc +``` + +# e2e environment using `multichain-testing` +using starship +``` +make teardown ; make stop; make stop-forward; make clean; make; make port-forward +``` + +# e2e workspaces +``` +yarn workspace dapp-agoric-orca-contract build; yarn workspace dapp-agoric-orca-contract e2e +``` + +# note +Troubleshooting remote calls + +If an ordinary synchronous call (obj.method()) fails because the method doesn't exist, the obj may be remote, in which case E(obj).method() might work. + +# ensure to override the chain registry (from inside multichain-testing): + +``` +yarn build (from agoric-sdk root) +make override-chain-registry +``` + +# funding on osmosis +```console +osmosisd tx bank send faucet osmo1dw3nep8yqy5szzxn6hmma6j2z77vp4wz8tkh0w3gyrruwny0w03s070kaa 299999999uosmo --chain-id osmosislocal --gas-adjustment 2 --gas auto --from faucet --gas-prices 0.0025uosmo +``` + +example rpc for balances: +``` +http://127.0.0.1:26657/abci_query?path=%22/cosmos.bank.v1beta1.Query/AllBalances%22&data=%22%5Cn-agoric12j5kzvrwunqvrga5vm4zpy3mkeh3lvyld0amz5%22 +``` + +# tmp fund ica +```console +agd tx bank send keplr1 agoric15ch7da0d8nvqc8hk6dguq4ext0lvskpjcwm3patf8sygm63chmpqjlzt74 1000uist -y --chain-id agoriclocal +``` + +# \ No newline at end of file diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..2cb92b6 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,332 @@ + +# Trouble Shooting Issues & their Solutions +1) SyntaxError#2: Unexpected token b in JSON at position 0 +confusing bundle ids for bundles, need to prepend with "@" + + +2) RangeError: Expected "[undefined]" is same as "Interface" +endo/patterns issue + +3) get E$1: undefined variable +using another E import, because it gets stripped by the rollup older versions of dapps use +```javascript +import { E } from '@endo/far'; +``` + +the rollup intends to strip that import: +https://github.com/Agoric/dapp-orchestration-basics/blob/921255ed33bd828843a89d73f64aeb82939dd78b/contract/rollup.config.mjs#L5-L7 + +4) SyntaxError: Possible HTML comment rejected at : +```html +// ISSUE IMPORTING THIS, which promted yarn link: +/* + [!] (plugin configureBundleID) TypeError: Failed to load module "./src/orchdev.contract.js" in package "file:///Users/jovonni/Documents/projects/experiments/orca/contract/" (1 underlying failures: Cannot find external module "@agoric/orchestration/src/exos/stakingAccountKit.js" in package file:///Users/jovonni/Documents/projects/experiments/orca/contract/ +src/orchdev.proposal.js +*/ +``` + +5) possible import rejection SES +check for `bn.js` containing `while (j-- > 0) {` + +we can check for this from outside the container: +``` +kubectl exec -it agoriclocal-genesis-0 -- cat ./node_modules/bn.js/lib/bn.js | grep "j\-\-" +``` + +If the file is there, we can do `make copy-bn-js ` + +6) +``` +v38: Error#1: privateArgs: (an undefined) - Must be a copyRecord to match a copyRecord pattern: {"marshaller":"[match:remotable]","orchestration":"[match:remotable]","storageNode":"[match:remotable]","timer":"[match:remotable]"} +``` +ensure privateArgs adheres to the format + +7) +``` +privateArgs: timer: (an object) - Must be a remotable TimerService, not promise +``` +ensure to pass the result of the promise, not the promise: `timer: await chainTimerService` + +8) +ensure to install +``` +yarn add typescript --dev +npm install -g rollup +``` + +9) +``` +yarn add @agoric/vow@0.1.1-upgrade-16-fi-dev-8879538.0 +yarn add @agoric/orchestration@0.1.1-upgrade-16-dev-d45b478.0 +npm install rollup +``` + +10) +``` +AssertionError [ERR_ASSERTION] [ERR_ASSERTION]: The expression evaluated to a falsy value: + + assert(refs.runnerChain) +``` + +11) +``` + AssertionError [ERR_ASSERTION] [ERR_ASSERTION]: The expression evaluated to a falsy value: +``` +`"@endo/patterns": "^1.4.0"` throws this error when used in devdependencies, when running tests, just remove + +Note: also remove all resolutions from root package.json, especially if you see this: +``` +Uncaught exception in test/test-deploy-tools.js + + RangeError: Expected "[undefined]" is same as "Interface" + + ✘ test/test-deploy-tools.js exited with a non-zero exit code: 1 + + Uncaught exception in test/test-orca-contract.js + + RangeError: Expected "[undefined]" is same as "Interface" + + Uncaught exception in test/test-bundle-source.js + + RangeError: Expected "[undefined]" is same as "Interface" + + ✘ No tests found in test/test-build-proposal.js + ✘ test/test-orca-contract.js exited with a non-zero exit code: 1 + ✘ test/test-bundle-source.js exited with a non-zero exit code: 1 + ─ +``` + +12) +``` +v43: Error#1: Cannot find file for internal module "./src/exos/cosmosOrchestrationAccount.js" (with candidates "./src/exos/cosmosOrchestrationAccount.js", "./src/exos/cosmosOrchestrationAccount.js.js", "./src/exos/cosmosOrchestrationAccount.js.json", " +``` + +inspect the container, and look at the module in question: +``` +head node_modules/@agoric/orchestration/package.json +``` + +double check the package.json `version`, to ensure resolution isn't overrriding a package on `yarn install` etc. + +13) +``` +xsnap: v52: Error: methodGuard: guard:methodGuard: (an object) - Must match one of [{"argGuards":"[match:arrayOf]","callKind":"sync","optionalArgGuards":"[match:or]","restArgGuard":"[match:or]","returnGuard":"[match:or]"},{"argGuards":"[match:arrayOf]","callKind":"async","optionalArgGuards":"[match:or]","restArgGuard":"[match:or]","returnGuard":"[Seen]"}] + +``` + +Ensure +```javascript +makeAcountInvitationMaker: M.call().returns(M.promise()), +``` +updated syntax: +```javascript +makeAccountInvitationMaker: M.callWhen().returns(InvitationShape) +``` + +14) +``` +Cannot find file for internal module "./vat.js" +``` + +the version of `@agoric/vow` should be kept updated to `@dev` for now to keep up. + +15) +``` +ReferenceError#1: accountsStorageNode: get E: undefined variable +``` + +Make sure `privateArgs` adheres to the correct shape expected or else any subsequent call to something like this will fail: + +```javascript +E(storageNode).makeChildNode('accounts'), +``` + +```javascript +export const meta = harden({ + privateArgsShape: { + orchestration: M.remotable('orchestration'), + storageNode: StorageNodeShape, + marshaller: M.remotable('marshaller'), + timer: TimerServiceShape, + }, +}); +export const privateArgsShape = meta.privateArgsShape; +``` + +16) +``` +Error#1: redefinition of durable kind " Durable Publish Kit " +``` + +```javascript +const { makeRecorderKit } = prepareRecorderKitMakers(baggage, marshaller); +``` +it throwing this error. `provideOrchestration` also calls `prepareRecorderKitMakers`. Also ensure the `remotePowers` has the following keys: +```javascript +orchestrationService: remotePowers.orchestration, +timerService: remotePowers.timer, +``` + +Because `makeOrchestrationFacade` expects the following remotePowers structure: +```javascript +/** + * + * @param {{ + * zone: Zone; + * timerService: Remote | null; + * zcf: ZCF; + * storageNode: Remote; + * orchestrationService: Remote | null; + * localchain: Remote; + * }} powers +``` + +17) +``` +TypeError: orchestrate: no function +``` + +If i log `asyncFlowTools`: +``` +asyncFlowTools +2024-06-30T02:33:01.781Z SwingSet: vat: v38: { prepareAsyncFlowKit: [Function: prepareAsyncFlowKit], asyncFlow: [Function: asyncFlow], adminAsyncFlow: Object [Alleged: AdminAsyncFlow] {}, allWokenP: Promise [Promise] {} } +``` + +``` +{ + "name": "@agoric/async-flow", + "version": "0.1.1-upgrade-16-fi-dev-8879538.0+8879538", +``` + +this is the `orchestrate` function in `@agoric/orchestration@0.1.1-upgrade-16-fi-dev-8879538.0+8879538` +```javascript +orchestrate(durableName, ctx, fn) { + /** @type {Orchestrator} */ + const orc = { + async getChain(name) { + if (name === 'agoric') { + return makeLocalChainFacade(localchain); + } + return makeRemoteChainFacade(name); + }, + makeLocalAccount() { + return E(localchain).makeAccount(); + }, + getBrandInfo: anyVal, + asAmount: anyVal, + }; + return async (...args) => fn(orc, ctx, ...args); +}, +``` + +Here is the same `orchestrate` function in `@agoric/orchestration@00.1.1-dev-6bc363b.0+6bc363b`: + +```javascript +orchestrate(durableName, hostCtx, guestFn) { + const subZone = zone.subZone(durableName); + const hostOrc = makeOrchestrator(); + const [wrappedOrc, wrappedCtx] = prepareEndowment(subZone, 'endowments', [ + hostOrc, + hostCtx, + ]); + const hostFn = asyncFlow(subZone, 'asyncFlow', guestFn); + const orcFn = (...args) => + // TODO remove the `when` after fixing the return type + // to `Vow` + when(hostFn(wrappedOrc, wrappedCtx, ...args)); + return harden(orcFn); +}, +``` + +Hypothesis: `prepareEndowment` doesn't exist, so version issue + + +18) +If you see `x.js` can't be resolved from an `@agoric/...` npm package, there may be a version mismatch where that version isn't exporting said file. Should be fixed with more stable versions eventually. + +19) +``` +Error#1: In "makeAccountInvitation" method of (OrcaFacet): result: (an object) - Must be a remotable Invitation, not promise +``` + +ensure your public facet returns the result of a promise, not the promise: + +```javascript +const publicFacet = zone.exo( + 'OrcaFacet', + M.interface('OrcaFacet', { + makeAccountInvitation: M.call().returns(M.promise()), + }), + { + async makeAccountInvitation() { // make sure NOT to use async here + const invitation = await zcf.makeInvitation( + createAccounts, + 'Create accounts', + undefined, + harden({ + give: {}, + want: {}, + exit: M.any(), + }), + ); + return invitation; + }, + }, +); + +``` + +``` +2024-07-01T19:16:10.669Z SwingSet: vat: v104: Warning for now: vow expected, not promise Promise [Promise] {} (Error#1) +2024-07-01T19:16:10.739Z SwingSet: xsnap: v104: Error#2: value for vow is not durable: slot 0 of { body: '#{"#tag":"Vow","payload":{"vowV0":"$0.Alleged: VowInternalsKit vowV0"}}', slots: [ 'o+40' ] } +2024-07-01T19:16:10.739Z SwingSet: xsnap: v104: Error: value for (a string) is not durable: slot 0 of (an object) +``` + +Make sure the offer handlers are in the top-level scope, so they don't inherit any "side effects" + + + + +20) +```js +await makeAccountInvitation() { +``` +instead of: +```js +makeAccountInvitation() { +``` + +also the function signature that works is: +```js + +/** + * handler function for creating and managing accounts //* Xparam {object} offerArgs + * @param {Orchestrator} orch + * @param {undefined} _ctx + * @param {ZCFSeat} seat + * @param {{ chainName: string }} offerArgs + */ +const createAccountsFn = async (orch, _ctx, seat, {chainName}) => {``` + +some types were not correct +``` + +Then also, fron the client: + +```js +wallet?.makeOffer( + { + source: 'contract', + instance, + publicInvitationMaker: 'makeOrchAccountInvitation', + // publicInvitationMaker: 'makeAccountInvitation', + // source: 'agoricContract', + // instancePath: ['basicFlows'], + // callPipe: [['makeOrchAccountInvitation']], + }, + { give, want }, + { chainName: "osmosis"}, +``` + +The offerArgs was empty + diff --git a/contract/Makefile b/contract/Makefile index 37be559..0261e8c 100644 --- a/contract/Makefile +++ b/contract/Makefile @@ -258,11 +258,6 @@ e2e: yarn run build:deployer make copy-project kubectl exec -i agoriclocal-genesis-0 -c validator -- bash -c "yarn install" - kubectl exec -i agoriclocal-genesis-0 -c validator -- bash -c "yarn add @endo/patterns@1.3.0" - kubectl exec -i agoriclocal-genesis-0 -c validator -- bash -c "yarn add @agoric/orchestration@0.1.1-dev-9c9e5cf.0" - kubectl exec -i agoriclocal-genesis-0 -c validator -- bash -c "yarn add @agoric/vow@0.1.1-dev-9c9e5cf.0" - kubectl exec -i agoriclocal-genesis-0 -c validator -- bash -c "yarn add @agoric/async-flow@0.1.1-dev-9c9e5cf.0" - make copy-bn-js kubectl exec -i agoriclocal-genesis-0 -c validator -- bash -c "yarn build:deployer" kubectl exec -i agoriclocal-genesis-0 -c validator -- bash -c "yarn node scripts/deploy-contract.js --install src/orca.contract.js --eval src/orca.proposal.js" kubectl exec -i agoriclocal-genesis-0 -c validator -- bash -c "yarn node scripts/deploy-contract.js --install src/orca.contract.js --eval src/orca.proposal.js" diff --git a/contract/src/orca.contract.js b/contract/src/orca.contract.js index ce06ea6..9d64538 100644 --- a/contract/src/orca.contract.js +++ b/contract/src/orca.contract.js @@ -1,10 +1,7 @@ import { AmountShape } from '@agoric/ertp'; import { makeTracer } from '@agoric/internal'; import { withOrchestration } from '@agoric/orchestration/src/utils/start-helper.js'; -import { atomicTransfer } from '@agoric/zoe/src/contractSupport/index.js'; import { InvitationShape } from '@agoric/zoe/src/typeGuards.js'; -import { Fail } from '@endo/errors'; -import { E } from '@endo/far'; import { M } from '@endo/patterns'; import * as flows from './orca.flows.js'; @@ -73,110 +70,14 @@ const contract = async ( zcf, privateArgs, zone, - { orchestrateAll, vowTools, zoeTools }, + { orchestrateAll, zoeTools }, ) => { trace('inside start function: v1.1.95'); trace('privateArgs', privateArgs); - const wrapper = () => { - const transfer = vowTools.retriable( - zone, - 'transfer', - /** - * @type {Transfer} - */ - async ( - srcSeat, - localAccount, - remoteAccount, - give, - amt, - localAddress, - remoteAddress, - ) => { - !srcSeat.hasExited() || Fail`The seat cannot have exited.`; - const { zcfSeat: tempSeat, userSeat: userSeatP } = - zcf.makeEmptySeatKit(); - trace('tempSeat:', tempSeat); - const userSeat = await userSeatP; - trace('userSeat:', userSeat); - trace('storageNode', privateArgs.storageNode); - atomicTransfer(zcf, srcSeat, tempSeat, give); - tempSeat.exit(); - - const pmt = await E(userSeat).getPayout('Deposit'); - trace('pmt:', pmt); - trace('amt:', amt); - - // NOTE: with watch - // const promises = Object.entries(give).map(async ([kw, _amount]) => { - // trace("kw::", kw) - // trace("_amount", _amount) - // trace("amt", amt) - // }); - // const watcher = zone.exo( - // `watcher-transfer-${localAddress.value}-to-${remoteAddress.value}`, // Error: key (a string) has already been used in this zone and incarnation -- perhaps use timestamp or offerid as well? - // M.interface('watcher for transfer', { - // onFulfilled: M.call(M.any()).optional(M.any()).returns(VowShape), - // } - // ), - // { - // /** - // * @param {any} _result - // * @param {bigint} value - // */ - // onFulfilled( - // _result, - // value - // ) { - // trace("inside onFulfilled:", value) - // return watch(localAccount.transfer( - // { - // denom: "ubld", - // value: value/2n, - // }, - // remoteAddress - // )) - // }, - // }, - // ); - // trace("about to watch transfer, watcher v0.16") - // trace("watcher", watcher) - // watch( - // E(localAccount).deposit(pmt), - // watcher, - // BigInt(amt.value), - // ); - // await Promise.all(promises); - - // NOTE: without watcher - await E(localAccount).deposit(pmt); - await localAccount.transfer( - { - denom: 'ubld', - value: amt.value / 2n, - }, - remoteAddress, - ); - - // const localAccountBalance = await localAccount.getBalance(amt.brand) - // const remoteAccountbalance = await remoteAccount.getBalance(amt.brand) - // trace("localaccount balance: ", localAccountBalance); - // trace("remoteaccount balance: ", remoteAccountbalance); - }, - ); - return harden({ - transfer, - }); - }; - - const wrap = wrapper(); - trace('wrapper.transfer', wrapper); - // @ts-expect-error XXX ZCFSeat not Passable const { makeAccount, makeCreateAndFund } = orchestrateAll(flows, { localTransfer: zoeTools.localTransfer, - transfer: wrap.transfer, // write: E(storageNode).write), // makeChildNode: E(storageNode).makeChildNode, // setValue: E(storageNode).setValue, diff --git a/contract/src/orca.flows.js b/contract/src/orca.flows.js index 2d1d373..e1bfeec 100644 --- a/contract/src/orca.flows.js +++ b/contract/src/orca.flows.js @@ -44,7 +44,7 @@ harden(makeAccount); * * @param {Orchestrator} orch * @param {object} ctx - * @param {Transfer} ctx.transfer + * @param {ZoeTools['localTransfer']} ctx.localTransfer * @param {StorageNode['setValue']} ctx.setValue * @param {ZCFSeat} seat * @param {{ chainName: string }} offerArgs @@ -52,7 +52,7 @@ harden(makeAccount); export const makeCreateAndFund = async ( orch, { - transfer, + localTransfer, // write, // makeChildNode, setValue, @@ -100,14 +100,12 @@ export const makeCreateAndFund = async ( trace('remoteAddress', remoteAddress); trace('fund new orch account'); trace('seat', seat); - trace('transfer', transfer); - await transfer( - seat, - localAccount, - remoteAccount, - give, - amt, - localAddress, + await localTransfer(seat, localAccount, give); + await localAccount.transfer( + { + denom: 'ubld', + value: amt.value / 2n, + }, remoteAddress, ); seat.exit(); diff --git a/images/orca.png b/images/orca.png new file mode 100644 index 0000000..de436a2 Binary files /dev/null and b/images/orca.png differ diff --git a/images/orca2.png b/images/orca2.png new file mode 100644 index 0000000..3b9f682 Binary files /dev/null and b/images/orca2.png differ diff --git a/images/ui.png b/images/ui.png new file mode 100644 index 0000000..97b4c3b Binary files /dev/null and b/images/ui.png differ diff --git a/ui/package.json b/ui/package.json index 10b76c4..33e4cb7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -3,29 +3,32 @@ "private": true, "version": "0.0.0", "type": "module", + "packageManager": "yarn@4.3.1", "scripts": { "dev": "vite", - "build": "tsc && vite build", - "lint": "tsc && eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "build": "tsc && NODE_OPTIONS=--max-old-space-size=4096 vite build", + "lint": "yarn tsc && eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "lint:fix": "yarn lint --fix", "preview": "vite preview", "test": "exit 0", "test:e2e": "exit 0" }, "dependencies": { - "@agoric/react-components": "0.1.1-dev-8fc28e8.0", + "@agoric/react-components": "^0.1.1-dev-ca0ddde.0", "@agoric/ui-components": "^0.3.9-u13.0", "@agoric/web-components": "0.15.1-dev-8fc28e8.0", "buffer": "^6.0.3", "chain-registry": "^1.28.1", "cosmos-kit": "^2.19.0", - "daisyui": "^4.7.2", - "react": "^18.2.0", + "daisyui": "^4.12.10", + "react": "^18.3.1", "react-daisyui": "^5.0.0", "react-dom": "^18.2.0", "ses": "^1.2.0" }, "devDependencies": { + "@interchain-ui/react": "^1.23.24", + "@keplr-wallet/types": "^0.12.121", "@types/react": "^18.2.56", "@types/react-dom": "^18.2.19", "@typescript-eslint/eslint-plugin": "^7.0.2", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index ac99b48..a69b4e3 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -3,29 +3,41 @@ import { AgoricProvider } from '@agoric/react-components'; import { Navbar } from './components/Navbar'; import { Tabs } from './components/Tabs'; import { wallets } from 'cosmos-kit'; +import { ThemeProvider, useTheme } from '@interchain-ui/react'; import '@agoric/react-components/dist/style.css'; +// import { Button, Modal } from 'react-daisyui'; function App() { + const { themeClass } = useTheme(); + return ( - - - - - - + +
+ + + + + + +
+
); } diff --git a/ui/src/components/Navbar.tsx b/ui/src/components/Navbar.tsx index ac652e5..f7074fc 100644 --- a/ui/src/components/Navbar.tsx +++ b/ui/src/components/Navbar.tsx @@ -1,22 +1,7 @@ import { ConnectWalletButton } from '@agoric/react-components'; import { NetworkDropdown } from '@agoric/react-components'; -import { ThemeProvider, useTheme } from '@interchain-ui/react'; - -const localnet = { - testChain: { - chainId: 'agoriclocal', - chainName: 'agoric-local', - }, - apis: { - rest: ['http://localhost:1317'], - rpc: ['http://localhost:26657'], - iconUrl: '/agoriclocal.svg', // Optional icon for dropdown display - }, -}; const Navbar = () => { - const { themeClass } = useTheme(); - return (
{/* Agoric logo */} @@ -28,17 +13,13 @@ const Navbar = () => { {/* dApp title */}
{/* network selector */} - -
-
- -
-
-
+
+ +
{/* connect wallet button */}
diff --git a/ui/src/components/Notifications.tsx b/ui/src/components/Notifications.tsx new file mode 100644 index 0000000..bc55040 --- /dev/null +++ b/ui/src/components/Notifications.tsx @@ -0,0 +1,50 @@ +import { Toast, Alert, Button } from 'react-daisyui'; +import { DynamicToastChild } from './Tabs'; +import { Dispatch, SetStateAction } from 'react'; + +const daisyUiAlertClass = (status: string) => { + switch (status) { + case 'info': + return 'daisyui-alert-info'; + case 'success': + return 'daisyui-alert-success'; + case 'warning': + return 'daisyui-alert-warning'; + case 'error': + return 'daisyui-alert-error'; + default: + return ''; + } +}; + +const Notifications = (props: { + notifications: DynamicToastChild[]; + setNotifications: Dispatch>; +}) => { + const handleRemoveToast = (index: number) => { + props.setNotifications(notifications => + notifications.filter((_, i) => i !== index), + ); + }; + + return ( + + {props.notifications.map((alert, index) => ( + +
+

{alert.text}

+
+ +
+ ))} +
+ ); +}; + +export { Notifications }; diff --git a/ui/src/components/Orchestration/AccountList.tsx b/ui/src/components/Orchestration/AccountList.tsx new file mode 100644 index 0000000..dd5527a --- /dev/null +++ b/ui/src/components/Orchestration/AccountList.tsx @@ -0,0 +1,104 @@ +const tokenLogos = { + ubld: 'https://assets.coingecko.com/coins/images/24487/large/agoric_bld_logo.png?1696523668', + uist: 'https://inter.trade/static/inter-protocol-logo-symbol-color-64318316bdb96c351674e3157a9f7546.png', + ibc: 'https://cdn-icons-png.flaticon.com/512/566/566295.png', + default: 'https://cdn-icons-png.flaticon.com/512/566/566295.png', +}; + +const AccountList = ({ + balances, + loadingDeposit, + handleDeposit, + loadingWithdraw, + handleWithdraw, + loadingStake, + handleStake, + loadingUnstake, + handleUnstake, + guidelines, +}) => { + return ( +
+

Accounts

+ {balances.map((balance, idx) => ( +
+
+
+
Address
+
+ {balance.address.slice(0, 10)}...{balance.address.slice(-10)} +
+
+ +
+ {balance.balances.map((bal, idx) => ( +
+
+
+
+ {`${bal.denom} +
+
+
+
+ {bal.denom.toUpperCase()} +
+
{bal.amount}
+
+ ))} +
+
+ +
+
+ + + + +
+
+
+ ))} +
+ ); +}; + +export default AccountList; diff --git a/ui/src/components/Orchestration/ChainSelector.tsx b/ui/src/components/Orchestration/ChainSelector.tsx new file mode 100644 index 0000000..964c361 --- /dev/null +++ b/ui/src/components/Orchestration/ChainSelector.tsx @@ -0,0 +1,14 @@ +const ChainSelector = ({ setSelectedChain }) => ( + +); + +export default ChainSelector; diff --git a/ui/src/components/Orchestration/CreateAccountButton.tsx b/ui/src/components/Orchestration/CreateAccountButton.tsx new file mode 100644 index 0000000..1e8f80b --- /dev/null +++ b/ui/src/components/Orchestration/CreateAccountButton.tsx @@ -0,0 +1,72 @@ +import { FaUserPlus, FaWallet } from 'react-icons/fa'; // Importing icons from react-icons + +const CreateAccountButton = ({ + handleCreateAccount, + handleCreateAndFund, + loadingCreateAccount, + loadingCreateAndFund, +}) => ( +
+ + +
+); + +export default CreateAccountButton; diff --git a/ui/src/components/Orchestration/FetchBalances.tsx b/ui/src/components/Orchestration/FetchBalances.tsx new file mode 100644 index 0000000..d510195 --- /dev/null +++ b/ui/src/components/Orchestration/FetchBalances.tsx @@ -0,0 +1,49 @@ +import { StargateClient } from '@cosmjs/stargate'; + +const rpcEndpoints = { + osmosis: 'http://127.0.0.1:26655', + agoric: 'http://127.0.0.1:26657', +}; + +export const fetchBalances = async (addresses: string[]) => { + return Promise.all( + addresses.map(async address => { + console.log('address', address); + let chain = ''; + if (address.startsWith('osmo1')) { + chain = 'osmosis'; + } else if (address.startsWith('agoric1')) { + chain = 'agoric'; + } else { + return { + address, + balances: [], + }; + } + + const rpcEndpoint = rpcEndpoints[chain]; + try { + const balance = await fetchBalanceFromRpc(address, rpcEndpoint); + return { + address, + balances: balance, + }; + } catch (e) { + console.log('e:', e); + return { + address, + balances: [], + }; + } + }), + ); +}; + +const fetchBalanceFromRpc = async (address, rpcEndpoint) => { + const client = await StargateClient.connect(rpcEndpoint); + const balances = await client.getAllBalances(address); + return balances.map(coin => ({ + denom: coin.denom, + amount: coin.amount, + })); +}; diff --git a/ui/src/components/Orchestration/KeplrInitializer.tsx b/ui/src/components/Orchestration/KeplrInitializer.tsx new file mode 100644 index 0000000..0dfcdeb --- /dev/null +++ b/ui/src/components/Orchestration/KeplrInitializer.tsx @@ -0,0 +1,45 @@ +export const initializeKeplr = async () => { + await window.keplr.experimentalSuggestChain({ + chainId: 'osmosislocal', + chainName: 'Osmosis Local', + rpc: 'http://127.0.0.1:26655', + rest: 'http://127.0.0.1:1315', + bip44: { + coinType: 118, + }, + bech32Config: { + bech32PrefixAccAddr: 'osmo', + bech32PrefixAccPub: 'osmopub', + bech32PrefixValAddr: 'osmovaloper', + bech32PrefixValPub: 'osmovaloperpub', + bech32PrefixConsAddr: 'osmovalcons', + bech32PrefixConsPub: 'osmovalconspub', + }, + currencies: [ + { + coinDenom: 'OSMO', + coinMinimalDenom: 'uosmo', + coinDecimals: 6, + }, + ], + feeCurrencies: [ + { + coinDenom: 'OSMO', + coinMinimalDenom: 'uosmo', + coinDecimals: 6, + }, + ], + stakeCurrency: { + coinDenom: 'OSMO', + coinMinimalDenom: 'uosmo', + coinDecimals: 6, + }, + // @ts-expect-error XXX typedefs + coinType: 118, + gasPriceStep: { + low: 0.01, + average: 0.025, + high: 0.04, + }, + }); +}; diff --git a/ui/src/components/Orchestration/MakeAccount.tsx b/ui/src/components/Orchestration/MakeAccount.tsx new file mode 100644 index 0000000..f3593e9 --- /dev/null +++ b/ui/src/components/Orchestration/MakeAccount.tsx @@ -0,0 +1,567 @@ +import { AgoricWalletConnection, useAgoric } from '@agoric/react-components'; +import { SigningStargateClient, StargateClient } from '@cosmjs/stargate'; +import { useContext, useEffect, useRef, useState } from 'react'; +import { Button } from 'react-daisyui'; +import { NotificationContext } from '../../context/NotificationContext'; +import { useContractStore } from '../../store/contract'; +import { DynamicToastChild } from '../Tabs'; + +const rpcEndpoints = { + osmosis: 'http://127.0.0.1:26655', + agoric: 'http://127.0.0.1:26657', +}; + +const initializeKeplr = async () => { + await window.keplr.experimentalSuggestChain({ + chainId: 'osmosislocal', + chainName: 'Osmosis Local', + rpc: 'http://127.0.0.1:26655', ///port from starshp + rest: 'http://127.0.0.1:1315', //port from starship + bip44: { + coinType: 118, + }, + bech32Config: { + bech32PrefixAccAddr: 'osmo', + bech32PrefixAccPub: 'osmopub', + bech32PrefixValAddr: 'osmovaloper', + bech32PrefixValPub: 'osmovaloperpub', + bech32PrefixConsAddr: 'osmovalcons', + bech32PrefixConsPub: 'osmovalconspub', + }, + currencies: [ + { + coinDenom: 'OSMO', + coinMinimalDenom: 'uosmo', + coinDecimals: 6, + }, + ], + feeCurrencies: [ + { + coinDenom: 'OSMO', + coinMinimalDenom: 'uosmo', + coinDecimals: 6, + }, + ], + stakeCurrency: { + coinDenom: 'OSMO', + coinMinimalDenom: 'uosmo', + coinDecimals: 6, + }, + // @ts-expect-error XXX typedefs + coinType: 118, + gasPriceStep: { + low: 0.01, + average: 0.025, + high: 0.04, + }, + }); +}; + +const fetchBalances = async addresses => { + return Promise.all( + addresses.map(async address => { + console.log('address', address); + let chain = ''; + if (address.startsWith('osmo1')) { + chain = 'osmosis'; + } else if (address.startsWith('agoric1')) { + chain = 'agoric'; + } else { + return { + address, + balances: [], + }; + } + + const rpcEndpoint = rpcEndpoints[chain]; + try { + const balance = await fetchBalanceFromRpc(address, rpcEndpoint); + return { + address, + balances: balance, + }; + } catch (e) { + console.log('e:', e); + return { + address, + balances: [], + }; + } + }), + ); +}; + +const fetchBalanceFromRpc = async (address, rpcEndpoint) => { + const client = await StargateClient.connect(rpcEndpoint); + const balances = await client.getAllBalances(address); + return balances.map(coin => ({ + denom: coin.denom, + amount: coin.amount, + })); +}; + +const makeAccountOffer = async ( + wallet: AgoricWalletConnection, + addNotification: (arg0: DynamicToastChild) => void, + selectedChain: string, +) => { + if (!selectedChain) { + addNotification({ + text: `Please Select Chain`, + status: 'error', + }); + return; + } + const { instances } = useContractStore.getState(); + // const instance = instances?.['basicFlows']; + const instance = instances?.['orca']; + + if (!instance) throw Error('no contract instance'); + + const want = {}; + const give = {}; + + // const makeAccountofferId = `makeAccount-${Date.now()}`; + const makeAccountofferId = Date.now(); + + // Make the initial offer + wallet?.makeOffer( + { + source: 'contract', + instance, + // publicInvitationMaker: 'makeOrchAccountInvitation', + publicInvitationMaker: 'makeAccountInvitation', + // source: 'agoricContract', + // instancePath: ['basicFlows'], + // callPipe: [['makeOrchAccountInvitation']], + }, + { give, want }, + { chainName: selectedChain }, + // {}, + (update: { status: string; data?: unknown }) => { + if (update.status === 'error') { + console.log(update); + } + if (update.status === 'accepted') { + console.log(update); + } + if (update.status === 'refunded') { + console.log(update); + } + }, + makeAccountofferId, + ); +}; + +// TODO: this can be for making an account + +const MakeAccount = () => { + const { walletConnection } = useAgoric(); + const { addNotification } = useContext(NotificationContext); + + const icas = useContractStore(state => state.icas); + console.log('ica from inside makeaccount', icas); + + const [balances, setBalances] = useState([]); + const [selectedChain, setSelectedChain] = useState(''); + + //spinners + + const [loadingDeposit, setLoadingDeposit] = useState(false); + const [loadingWithdraw, setLoadingWithdraw] = useState(false); + const [loadingStake, setLoadingStake] = useState(false); + const [loadingUnstake, setLoadingUnstake] = useState(false); + const [loadingCreateAccount, setLoadingCreateAccount] = useState(false); + + //modal + // const [modalOpen, modalSetOpen] = useState(false); + // const handleToggle = () => modalSetOpen((prev) => !prev); + const [modalOpen, setModalOpen] = useState(false); + const modalRef = useRef(null); + + const handleToggle = () => { + if (modalRef.current) { + if (modalOpen) { + modalRef.current.close(); + } else { + modalRef.current.showModal(); + } + setModalOpen(!modalOpen); + } + }; + + useEffect(() => { + const loadBalances = async () => { + try { + const fetchedBalances = await fetchBalances(icas); + console.log('fetchedBalances'); + console.log(fetchedBalances); + setBalances(fetchedBalances); + } catch (error) { + console.error('Failed to fetch balances:', error); + } + }; + + // if (icas && icas.length > 0 && selectedChain) { + if (icas && icas.length > 0) { + loadBalances(); + } + }, [icas, selectedChain]); + + // //spinners + useEffect(() => { + document + .querySelectorAll('.loading-spinner') + .forEach((spinner: HTMLElement) => { + spinner.style.display = 'inline-block'; + }); + }, []); + + //modal + + const handleCreateAccount = () => { + setLoadingCreateAccount(true); + if (walletConnection) { + makeAccountOffer(walletConnection, addNotification!, selectedChain); + setLoadingCreateAccount(false); + } else { + addNotification!({ + text: 'error: please connect your wallet or check your connection to RPC endpoints', + status: 'error', + }); + setLoadingCreateAccount(false); + } + }; + + const handleDeposit = async address => { + setLoadingDeposit(true); + try { + // init osmo in wallet + await initializeKeplr(); + + let chain = ''; + if (address.startsWith('osmo1')) { + chain = 'osmosis'; + } else if (address.startsWith('agoric1')) { + chain = 'agoric'; + } else { + throw new Error('unsupported address prefix'); + } + + if (chain === 'agoric') { + await window.keplr.enable(`${chain}local`); + const offlineSigner = window.getOfflineSigner(`${chain}local`); + const accounts = await offlineSigner.getAccounts(); + + // const client = await SigningStargateClient.connectWithSigner(rpcEndpoints[chain], offlineSigner); + const client = await SigningStargateClient.connectWithSigner( + `${rpcEndpoints[chain]}`, + offlineSigner, + ); + + const sendMsg = { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + fromAddress: accounts[0].address, + toAddress: address, + amount: [{ denom: 'ubld', amount: '1000000' }], + }, + }; + + const fee = { + amount: [{ denom: 'ubld', amount: '5000' }], + gas: '200000', + }; + + const result = await client.signAndBroadcast( + accounts[0].address, + [sendMsg], + fee, + '', + ); + console.log(result); + if (result.code !== undefined && result.code !== 0) { + throw new Error(`failed to send message: ${result}`); + } + console.log('message sent successfully'); + } else { + await window.keplr.enable(`${chain}local`); + const offlineSigner = window.getOfflineSigner(`${chain}local`); + console.log('offlineSigner', offlineSigner); + const accounts = await offlineSigner.getAccounts(); + console.log('accounts', accounts); + + console.log(rpcEndpoints); + const client = await SigningStargateClient.connectWithSigner( + `${rpcEndpoints[chain]}`, + offlineSigner, + ); + + const sendMsg = { + typeUrl: '/ibc.applications.transfer.v1.MsgTransfer', + value: { + sourcePort: 'transfer', + sourceChannel: 'channel-0', + token: { denom: 'uosmo', amount: '1000000' }, + sender: accounts[0].address, + receiver: address, + timeoutTimestamp: (Math.floor(Date.now() / 1000) + 600) * 1e9, // 10 + }, + }; + + const fee = { + amount: [{ denom: 'ubld', amount: '5000' }], + gas: '200000', + }; + + console.log('accounts[0].address', accounts[0].address); + + const result = await client.signAndBroadcast( + accounts[0].address, + [sendMsg], + fee, + '', + ); + console.log(result); + if (result.code !== undefined && result.code !== 0) { + throw new Error(`Failed to send IBC transfer: ${result}`); + } + console.log('ibc transfer sent successfully'); + } + } catch (error) { + console.error('failed to deposit:', error); + } finally { + setLoadingDeposit(false); + } + }; + + const handleWithdraw = () => { + setLoadingWithdraw(true); + // setLoadingWithdraw(false); + }; + + const handleStake = () => { + setLoadingStake(true); + // setLoadingStake(false); + }; + + const handleUnstake = () => { + setLoadingUnstake(true); + // setLoadingUnstake(false); + }; + + const guidelines = true; + + return ( +
+
+
+
+

Accounts

+ {balances.map((balance, idx) => ( +
+ {' '} + {/* card */} +

+ Address: {balance.address.slice(0, 10)}... + {balance.address.slice(-10)} +

+ {balance.balances.map((bal, idx) => ( +

+ {bal.denom}: {bal.amount} +

+ ))} +
+
+ + + + + + + +
+
+
+ ))} +
+ +
+ + + +
+
+
+ + {/* modal work */} + +
+

Hello!

+

Click the button below to close

+
+
+ +
+
+
+
+ +
+ ); +}; + +export { MakeAccount }; diff --git a/ui/src/components/Orchestration/MakeOffer.tsx b/ui/src/components/Orchestration/MakeOffer.tsx new file mode 100644 index 0000000..1959382 --- /dev/null +++ b/ui/src/components/Orchestration/MakeOffer.tsx @@ -0,0 +1,95 @@ +import { AgoricWalletConnection } from '@agoric/react-components'; +import { DynamicToastChild } from '../Tabs'; +import { useContractStore } from '../../store/contract'; + +export const makeOffer = async ( + wallet: AgoricWalletConnection, + addNotification: (arg0: DynamicToastChild) => void, + selectedChain: string, + publicInvitationMaker: string, + offerArgs: Record, + setLoading: React.Dispatch>, + handleToggle: () => void, + setStatusText: React.Dispatch>, +) => { + if (!selectedChain) { + addNotification({ + text: `Please Select Chain`, + status: 'error', + }); + setLoading(false); + handleToggle(); + return; + } + + const { instances, brands } = useContractStore.getState(); + const instance = instances?.['orca']; + + if (!instance || !brands) { + setLoading(false); + handleToggle(); + throw Error('No contract instance or brands found.'); + } + + // fetch the BLD brand + const bldBrand = brands.BLD; + if (!bldBrand) { + setLoading(false); + handleToggle(); + throw Error('BLD brand not found.'); + } + + const want = {}; + const give = { Deposit: { brand: bldBrand, value: BigInt(1000) } }; + + const offerId = Date.now(); + + await wallet?.makeOffer( + { + source: 'contract', + instance, + publicInvitationMaker, + }, + { give, want }, + offerArgs, + (update: { status: string; data?: unknown }) => { + if (update.status === 'error') { + addNotification({ + text: `Offer update error: ${update.data}`, + status: 'error', + }); + setStatusText('Error during offer submission.'); + setLoading(false); + handleToggle(); + console.log(update); + } + if (update.status === 'accepted') { + addNotification({ + text: 'Offer accepted successfully', + status: 'success', + }); + setStatusText('Offer accepted. Processing...'); + handleToggle(); + setLoading(false); + } + if (update.status === 'refunded') { + addNotification({ + text: 'Offer was refunded', + status: 'error', + }); + setStatusText('Offer refunded.'); + setLoading(false); + handleToggle(); + console.log(update); + } + if (update.status === 'done') { + setStatusText('Operation completed successfully.'); + setLoading(false); + setTimeout(() => { + handleToggle(); + }, 1000); + } + }, + offerId, + ); +}; diff --git a/ui/src/components/Orchestration/Orchestration.tsx b/ui/src/components/Orchestration/Orchestration.tsx new file mode 100644 index 0000000..909d5e5 --- /dev/null +++ b/ui/src/components/Orchestration/Orchestration.tsx @@ -0,0 +1,384 @@ +import { useAgoric } from '@agoric/react-components'; +import { SigningStargateClient } from '@cosmjs/stargate'; +import { useContext, useEffect, useRef, useState } from 'react'; +import { NotificationContext } from '../../context/NotificationContext'; +import { useContractStore } from '../../store/contract'; +import AccountList from './AccountList'; +import ChainSelector from './ChainSelector'; +import CreateAccountButton from './CreateAccountButton'; +import { fetchBalances } from './FetchBalances'; +import { initializeKeplr } from './KeplrInitializer'; +import { makeOffer } from './MakeOffer'; +import RpcEndpoints from './RpcEndpoints'; + +const Orchestration = () => { + const { walletConnection } = useAgoric(); + const { addNotification } = useContext(NotificationContext); + const icas = useContractStore(state => state.icas); + const [balances, setBalances] = useState([]); + const [selectedChain, setSelectedChain] = useState(''); + const [loadingDeposit, setLoadingDeposit] = useState<{ + [key: string]: boolean; + }>({}); + const [loadingWithdraw, setLoadingWithdraw] = useState<{ + [key: string]: boolean; + }>({}); + const [loadingStake, setLoadingStake] = useState<{ [key: string]: boolean }>( + {}, + ); + const [loadingUnstake, setLoadingUnstake] = useState<{ + [key: string]: boolean; + }>({}); + const [loadingCreateAccount, setLoadingCreateAccount] = useState(false); + const [loadingCreateAndFund, setLoadingCreateAndFund] = useState(false); + const [modalContent, setModalContent] = useState(''); + const [modalOpen, setModalOpen] = useState(false); + const [modalAddress, setModalAddress] = useState(''); + const [selectedDenom, setSelectedDenom] = useState('uist'); + const [amount, setAmount] = useState(0); + const [statusText, setStatusText] = useState(''); + const modalRef = useRef(null); + const guidelines = false; + + const handleToggle = () => { + setModalOpen(prevState => !prevState); + }; + + useEffect(() => { + const loadBalances = async () => { + try { + const fetchedBalances = await fetchBalances(icas); + setBalances(fetchedBalances); + } catch (error) { + console.error('failed to fetch balances:', error); + } + }; + if (icas && icas.length > 0) { + loadBalances(); + } + }, [icas, selectedChain]); + + const openModal = (content: string, address: string = '') => { + setModalContent(content); + setModalAddress(address); + handleToggle(); + }; + + useEffect(() => { + if (modalRef.current) { + if (modalOpen) { + modalRef.current.showModal(); + } else { + modalRef.current.close(); + } + } + }, [modalOpen]); + + const handleCreateAccount = () => { + setLoadingCreateAccount(true); + setStatusText('Submitted'); + if (walletConnection) { + makeOffer( + walletConnection, + addNotification!, + selectedChain, + 'makeAccountInvitation', + { chainName: selectedChain }, + setLoadingCreateAccount, + handleToggle, + setStatusText, + ).catch(error => { + addNotification!({ + text: `Transaction failed: ${error.message}`, + status: 'error', + }); + setLoadingCreateAccount(false); + handleToggle(); + }); + } else { + addNotification!({ + text: 'Error: Please connect your wallet or check your connection to RPC endpoints', + status: 'error', + }); + setLoadingCreateAccount(false); + handleToggle(); + setLoadingCreateAccount(false); + handleToggle(); + } + }; + + const handleCreateAndFund = () => { + handleToggle(); + setLoadingCreateAndFund(true); + setStatusText('Submitted'); + if (walletConnection) { + openModal('Create & Fund Account...', selectedChain); + makeOffer( + walletConnection, + addNotification!, + selectedChain, + 'makeCreateAndFundInvitation', + { chainName: selectedChain }, // adjust as needed + setLoadingCreateAndFund, + handleToggle, + setStatusText, + ).catch(error => { + addNotification!({ + text: `Transaction failed: ${error.message}`, + status: 'error', + }); + setLoadingCreateAndFund(false); + handleToggle(); + }); + } else { + addNotification!({ + text: 'Error: Please connect your wallet or check your connection to RPC endpoints', + status: 'error', + }); + setLoadingCreateAndFund(false); + handleToggle(); + } + }; + + const handleDeposit = (address: string) => { + openModal('Deposit', address); + }; + + const executeDeposit = async () => { + setLoadingDeposit(prevState => ({ ...prevState, [modalAddress]: true })); + try { + await initializeKeplr(); + let chain = ''; + if (modalAddress.startsWith('osmo1')) { + chain = 'osmosis'; + } else if (modalAddress.startsWith('agoric1')) { + chain = 'agoric'; + } else { + throw new Error('unsupported address prefix'); + } + if (chain === 'agoric') { + await window.keplr.enable(`${chain}local`); + const offlineSigner = window.getOfflineSigner(`${chain}local`); + const accounts = await offlineSigner.getAccounts(); + const client = await SigningStargateClient.connectWithSigner( + `${RpcEndpoints[chain]}`, + offlineSigner, + ); + const sendMsg = { + typeUrl: '/cosmos.bank.v1beta1.MsgSend', + value: { + fromAddress: accounts[0].address, + toAddress: modalAddress, + amount: [{ denom: selectedDenom, amount: amount.toString() }], + }, + }; + const fee = { + amount: [{ denom: 'ubld', amount: '5000' }], + gas: '200000', + }; + const result = await client.signAndBroadcast( + accounts[0].address, + [sendMsg], + fee, + '', + ); + if (result.code !== undefined && result.code !== 0) { + throw new Error(`failed to send message: ${result}`); + } + console.log('message sent successfully'); + } else { + // await window.keplr.enable(`${chain}local`); + // const offlineSigner = window.getOfflineSigner(`${chain}local`); + await window.keplr.enable(`agoriclocal`); + const offlineSigner = window.getOfflineSigner(`agoriclocal`); + const accounts = await offlineSigner.getAccounts(); + // const client = await SigningStargateClient.connectWithSigner(`${RpcEndpoints[chain]}`, offlineSigner); + const client = await SigningStargateClient.connectWithSigner( + `${RpcEndpoints['agoric']}`, + offlineSigner, + ); + const sendMsg = { + typeUrl: '/ibc.applications.transfer.v1.MsgTransfer', + value: { + sourcePort: 'transfer', + sourceChannel: 'channel-0', //TODO: fetch correct channel id + token: { denom: selectedDenom, amount: amount.toString() }, + sender: accounts[0].address, + receiver: modalAddress, + timeoutTimestamp: (Math.floor(Date.now() / 1000) + 600) * 1e9, //10 + }, + }; + const fee = { + amount: [{ denom: 'ubld', amount: '5000' }], + gas: '200000', + }; + const result = await client.signAndBroadcast( + accounts[0].address, + [sendMsg], + fee, + '', + ); + console.log(result); + if (result.code !== undefined && result.code !== 0) { + throw new Error(`Failed to send IBC transfer: ${result}`); + } + console.log('IBC transfer sent successfully'); + } + } catch (error) { + console.error('failed to deposit:', error); + } finally { + setLoadingDeposit(prevState => ({ ...prevState, [modalAddress]: false })); + handleToggle(); + } + }; + + const handleWithdraw = (address: string) => { + openModal('Withdraw', address); + setLoadingWithdraw(prevState => ({ ...prevState, [address]: false })); + }; + + const handleStake = (address: string) => { + openModal('Stake', address); + setLoadingStake(prevState => ({ ...prevState, [address]: false })); + }; + + const handleUnstake = (address: string) => { + openModal('Unstake', address); + setLoadingUnstake(prevState => ({ ...prevState, [address]: false })); + }; + + return ( +
+
+
+ + +
+ + +
+
+
+ + {/* modal */} + +
+ +

{modalContent}

+ {modalContent === 'Create Account' && ( +
+

{statusText}

+ {loadingCreateAccount && ( + + )} +
+ )} + {modalContent === 'Deposit' && ( +
+ + +
+ + +
+
+ )} +
+
+
+ ); +}; + +export default Orchestration; diff --git a/ui/src/components/Orchestration/RpcEndpoints.tsx b/ui/src/components/Orchestration/RpcEndpoints.tsx new file mode 100644 index 0000000..48c3e23 --- /dev/null +++ b/ui/src/components/Orchestration/RpcEndpoints.tsx @@ -0,0 +1,6 @@ +const RpcEndpoints = { + osmosis: 'http://127.0.0.1:26655', + agoric: 'http://127.0.0.1:26657', +}; + +export default RpcEndpoints; diff --git a/ui/src/components/Orchestration/index.ts b/ui/src/components/Orchestration/index.ts new file mode 100644 index 0000000..914ad51 --- /dev/null +++ b/ui/src/components/Orchestration/index.ts @@ -0,0 +1 @@ +export { default } from './Orchestration'; diff --git a/ui/src/components/Proposals.tsx b/ui/src/components/Proposals.tsx deleted file mode 100644 index 2e2b50b..0000000 --- a/ui/src/components/Proposals.tsx +++ /dev/null @@ -1,259 +0,0 @@ -// import { useState, useContext, useEffect } from 'react'; -import { useState, useEffect } from 'react'; -import { AmountMath } from '@agoric/ertp'; -import { - ConnectWalletButton, - useAgoric, - AgoricWalletConnection, -} from '@agoric/react-components'; - -import { usePurse } from '../hooks/usePurse'; -import { stringifyAmountValue } from '@agoric/web-components'; -import type { CopyBag } from '../types'; -import { useContractStore } from '../store/contract'; -import { makeCopyBag } from '@endo/patterns'; - -const joinDao = (wallet: AgoricWalletConnection) => { - const { instance, brands } = useContractStore.getState(); - if (!instance) throw Error('no contract instance'); - - console.log(brands); - - const choices: [string, bigint][] = [['MembershipCard2', 1n]]; - - const choiceBag = makeCopyBag(choices); - console.log(brands); - - const want = { - NewMembership: { - brand: brands?.Membership, - value: choiceBag, - }, - DaoTokens: { - brand: brands?.DaoToken, - value: 10n, - }, - }; - - const give = {}; - - wallet?.makeOffer( - { - source: 'contract', - instance, - publicInvitationMaker: 'makeJoinInvitation', - }, - { give, want }, - undefined, - (update: { status: string; data?: unknown }) => { - if (update.status === 'error') { - console.log(`Offer error: ${update.data}`); - } - if (update.status === 'accepted') { - console.log('Offer accepted'); - } - if (update.status === 'refunded') { - console.log('Offer refunded'); - } - }, - ); -}; - -const Proposals = () => { - const [newTitle, setNewTitle] = useState(''); - const [newDetails, setNewDetails] = useState(''); - const { instance, brands, proposals } = useContractStore.getState(); - const daoPurse = usePurse('DaoToken'); - const membershipPurse = usePurse('Membership'); - - const { walletConnection } = useAgoric(); - - useEffect(() => { - // ... - }, [walletConnection, instance]); - - const handleCreateProposal = async (newTitle: string, newDetails: string) => { - if (!walletConnection) { - alert('Please connect your wallet.'); - return; - } - if (!newTitle || !newDetails) { - alert('Please fill in both title and details for the proposal.'); - return; - } - - try { - const { instance } = useContractStore.getState(); - - const createProposalOffer = { - give: {}, // nothing to give for creating a proposal - want: {}, - }; - - walletConnection.makeOffer( - { - source: 'contract', - instance, - publicInvitationMaker: 'createProposalInvitation', - }, - createProposalOffer, - { title: newTitle, details: newDetails }, - update => { - if (update.status === 'accepted') { - console.log('Proposal creation accepted:', update); - } else { - console.log('Proposal creation failed:', update); - } - }, - ); - } catch (error) { - console.error('Error creating proposal:', error); - alert('Failed to create proposal.'); - } - }; - - const voteOnProposal = async (proposalId, voteFor) => { - if (!walletConnection) { - alert('Please connect your wallet.'); - return; - } - - const voteOffer = { - give: { - Votes: { brand: brands?.DaoToken, value: 10n }, - CurrentMembership: { - brand: brands?.Membership, - value: makeCopyBag([]), - }, - }, - want: { - NewMembership: { brand: brands?.Membership, value: makeCopyBag([]) }, - }, - }; - - const voteDetails = { - voteFor: voteFor ? 10n : undefined, - voteAgainst: !voteFor ? 10n : undefined, - }; - - walletConnection.makeOffer( - { - source: 'contract', - instance, - publicInvitationMaker: 'makeVoteInvitation', - proposalId, - voteFor, - }, - voteOffer, - { proposalId: proposalId, ...voteDetails }, - update => { - console.log('Vote update:', update); - }, - ); - }; - - return ( -
- {/* Wallet Section */} -
-
-

Membership/Tokens

- - {walletConnection && ( -
-
- DaoToken: - {daoPurse ? ( - stringifyAmountValue( - AmountMath.make( - // @ts-expect-error XXX - daoPurse?.currentAmount.brand, - daoPurse?.currentAmount.value, - ), - daoPurse.displayInfo.assetKind, - daoPurse.displayInfo.decimalPlaces, - ) - ) : ( - Fetching balance... - )} -
-
- Membership: - {membershipPurse ? ( -
    - {( - membershipPurse.currentAmount.value as CopyBag - ).payload.map(([name, number]) => ( -
  • - {name}-{String(number)} -
  • - ))} -
- ) : ( - 'None' - )} -
- -
- )} -
-
- - {/* Proposals Section */} -
-
-

Proposals

- - {proposals?.map(proposal => ( -
-

{proposal.title.title}

-

{proposal.title.details}

- - -
- ))} - -

Create Proposal

-
- setNewTitle(e.target.value)} - /> -