diff --git a/.gitignore b/.gitignore index 48b215f2..ef52d23f 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ dist-ssr *.njsproj *.sln *.sw? + +./ao-contracts/curve-bonded-token/process-dist \ No newline at end of file diff --git a/ao-contracts/curve-bonded-token/.gitignore b/ao-contracts/curve-bonded-token/.gitignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/ao-contracts/curve-bonded-token/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/ao-contracts/curve-bonded-token/README.md b/ao-contracts/curve-bonded-token/README.md new file mode 100644 index 00000000..60eeab22 --- /dev/null +++ b/ao-contracts/curve-bonded-token/README.md @@ -0,0 +1,36 @@ +# pl-bonded-token + +AO contract created using [create-ao-contract](https://github.com/pawanpaudel93/create-ao-contract) featuring [Busted](https://luarocks.org/modules/lunarmodules/busted) for testing and seamless deployment via [ao-deploy](https://github.com/pawanpaudel93/ao-deploy). + +## Prerequisites + +1. Make sure you have [Lua](https://www.lua.org/start.html#installing) and [LuaRocks](https://github.com/luarocks/luarocks/wiki/Download) installed. + +2. Install [arweave](https://luarocks.org/modules/crookse/arweave) using LuaRocks for testing purposes. + + ```bash + luarocks install arweave + ``` + +3. **[Recommended]** Install [Lua Language Server](https://luals.github.io/#install) to make development easier, safer, and faster!. On VSCode, install extension: [sumneko.lua](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) + - Install AO & Busted addon using Lua Addon Manager. On VSCode, goto `View > Command Palette > Lua: Open Addon Manager` + +## Usage + +To install dependencies: + +```bash +npm install +``` + +To run tests: + +```bash +pnpm test +``` + +To deploy contract: + +```bash +pnpm deploy +``` diff --git a/ao-contracts/curve-bonded-token/aod.config.js b/ao-contracts/curve-bonded-token/aod.config.js new file mode 100644 index 00000000..3381f60d --- /dev/null +++ b/ao-contracts/curve-bonded-token/aod.config.js @@ -0,0 +1,15 @@ +import { defineConfig } from 'ao-deploy' + +export default defineConfig({ + 'curve-bonded-token': { + name: 'curve-bonded-token', + contractPath: 'src/contracts/curve_bonded_token.lua', + luaPath: './src/?.lua' + }, + 'curve-bonded-token-manager': { + name: 'curve-bonded-token-manager', + contractPath: 'src/contracts/curve_bonded_token_manager.lua', + luaPath: './src/?.lua', + outDir: '../../public/contracts' + } +}) diff --git a/ao-contracts/curve-bonded-token/package.json b/ao-contracts/curve-bonded-token/package.json new file mode 100644 index 00000000..58c1a55b --- /dev/null +++ b/ao-contracts/curve-bonded-token/package.json @@ -0,0 +1,18 @@ +{ + "name": "pl-bonded-token", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "build": "ao-deploy aod.config.js --build-only", + "deploy": "ao-deploy aod.config.js", + "test": "arweave test . --lpath='./src/?.lua;./src/?/?.lua;./src/?/init.lua;/Users/dexter/Library/Application Support/create-ao-contract-nodejs/aos-process/?.lua'" + }, + "type": "module", + "dependencies": { + "ao-deploy": "^0.5.0" + }, + "cacMetadata": { + "initVersion": "1.0.3" + }, + "packageManager": "pnpm@8.14.1" +} diff --git a/ao-contracts/curve-bonded-token/pnpm-lock.yaml b/ao-contracts/curve-bonded-token/pnpm-lock.yaml new file mode 100644 index 00000000..8f96d85e --- /dev/null +++ b/ao-contracts/curve-bonded-token/pnpm-lock.yaml @@ -0,0 +1,219 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + ao-deploy: + specifier: ^0.5.0 + version: 0.5.0 + +packages: + + /@fastify/busboy@2.1.1: + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + dev: false + + /@permaweb/ao-scheduler-utils@0.0.24: + resolution: {integrity: sha512-G6109Nz8+dQFPuG7mV8mz66kLVA+gl2uTSqU7qpaRwfujrWi6obM94CpmvyvAnrLo3dB29EYiuv7+KOKcns8ig==} + engines: {node: '>=18'} + dependencies: + lru-cache: 10.4.3 + ramda: 0.30.1 + zod: 3.23.8 + dev: false + + /@permaweb/aoconnect@0.0.58: + resolution: {integrity: sha512-vVxTdsXaWNzM+iGFIAnq7+/yktMO7HCiS5ss8s4+4bolUwh2zVWTKDjM4JsGNQXqlhyNU21hhy0V9DwaunuRjw==} + engines: {node: '>=18'} + dependencies: + '@permaweb/ao-scheduler-utils': 0.0.24 + buffer: 6.0.3 + debug: 4.3.7 + hyper-async: 1.1.2 + mnemonist: 0.39.8 + ramda: 0.30.1 + warp-arbundles: 1.0.4 + zod: 3.23.8 + transitivePeerDependencies: + - supports-color + dev: false + + /ao-deploy@0.5.0: + resolution: {integrity: sha512-MB5ebPHQ9ZGGNwah0rgz6FOHGVicaohXE2uAy4Z2gmwkYU7JtWAUmiW3B2mj6Q9cq9h2y9/XApd50kq6Oq6ZLQ==} + hasBin: true + dependencies: + '@permaweb/aoconnect': 0.0.58 + arweave: 1.15.5 + chalk: 5.3.0 + commander: 12.1.0 + jiti: 1.21.6 + p-limit: 4.0.0 + pretty-file-tree: 1.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /arconnect@0.4.2: + resolution: {integrity: sha512-Jkpd4QL3TVqnd3U683gzXmZUVqBUy17DdJDuL/3D9rkysLgX6ymJ2e+sR+xyZF5Rh42CBqDXWNMmCjBXeP7Gbw==} + dependencies: + arweave: 1.15.5 + dev: false + + /arweave@1.15.5: + resolution: {integrity: sha512-Zj3b8juz1ZtDaQDPQlzWyk2I4wZPx3RmcGq8pVJeZXl2Tjw0WRy5ueHPelxZtBLqCirGoZxZEAFRs6SZUSCBjg==} + engines: {node: '>=18'} + dependencies: + arconnect: 0.4.2 + asn1.js: 5.4.1 + base64-js: 1.5.1 + bignumber.js: 9.1.2 + dev: false + + /asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + dependencies: + bn.js: 4.12.0 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + dev: false + + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + + /base64url@3.0.1: + resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} + engines: {node: '>=6.0.0'} + dev: false + + /bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + dev: false + + /bn.js@4.12.0: + resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} + dev: false + + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + + /commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + dev: false + + /debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.3 + dev: false + + /hyper-async@1.1.2: + resolution: {integrity: sha512-cnpOgKa+5FZOaccTtjduac1FrZuSc38/ftCp3vYJdUMt+7c+uvGDKLDK4MTNK8D3aFjIeveVrPcSgUPvzZLopg==} + dev: false + + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /jiti@1.21.6: + resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} + hasBin: true + dev: false + + /lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + dev: false + + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: false + + /mnemonist@0.39.8: + resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==} + dependencies: + obliterator: 2.0.4 + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /obliterator@2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + dev: false + + /p-limit@4.0.0: + resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + yocto-queue: 1.1.1 + dev: false + + /pretty-file-tree@1.0.1: + resolution: {integrity: sha512-w6uf7qIl6GTx8QjPKuhz62AjVJIg6/YD8aiblq7oXbl4XhdZqtarKMftFVxWoII4JSxS20CUK9ixoTVsJLDIZg==} + dev: false + + /ramda@0.30.1: + resolution: {integrity: sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /undici@5.28.4: + resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} + engines: {node: '>=14.0'} + dependencies: + '@fastify/busboy': 2.1.1 + dev: false + + /warp-arbundles@1.0.4: + resolution: {integrity: sha512-KeRac/EJ7VOK+v5+PSMh2SrzpCKOAFnJICLlqZWt6qPkDCzVwcrNE5wFxOlEk5U170ewMDAB3e86UHUblevXpw==} + dependencies: + arweave: 1.15.5 + base64url: 3.0.1 + buffer: 6.0.3 + warp-isomorphic: 1.0.7 + dev: false + + /warp-isomorphic@1.0.7: + resolution: {integrity: sha512-fXHbUXwdYqPm9fRPz8mjv5ndPco09aMQuTe4kXfymzOq8V6F3DLsg9cIafxvjms9/mc6eijzkLBJ63yjEENEjA==} + engines: {node: '>=16.8.0'} + dependencies: + buffer: 6.0.3 + undici: 5.28.4 + dev: false + + /yocto-queue@1.1.1: + resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==} + engines: {node: '>=12.20'} + dev: false + + /zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + dev: false diff --git a/ao-contracts/curve-bonded-token/process-dist/curve-bonded-token.lua b/ao-contracts/curve-bonded-token/process-dist/curve-bonded-token.lua new file mode 100644 index 00000000..ad3b5012 --- /dev/null +++ b/ao-contracts/curve-bonded-token/process-dist/curve-bonded-token.lua @@ -0,0 +1,881 @@ +-- module: "src.utils.mod" +local function _loaded_mod_src_utils_mod() + local bint = require('.bint')(256) + + local utils = { + add = function(a, b) + return tostring(bint(a) + bint(b)) + end, + subtract = function(a, b) + return tostring(bint(a) - bint(b)) + end, + multiply = function(a, b) + return tostring(bint(a) * bint(b)) + end, + divide = function(a, b) + return tostring(bint(a) / bint(b)) + end, + udivide = function(a, b) + return tostring(bint.udiv(bint(a), bint(b))) + end, + toBalanceValue = function(a) + return tostring(bint(a)) + end, + toNumber = function(a) + return tonumber(a) + end, + toSubUnits = function(val, denom) + return bint(val) * bint.ipow(bint(10), bint(denom)) + end + } + + return utils + +end + +_G.package.loaded["src.utils.mod"] = _loaded_mod_src_utils_mod() + +-- module: "src.handlers.token" +local function _loaded_mod_src_handlers_token() + local utils = require "src.utils.mod" + + local mod = {} + + --- @type Denomination + Denomination = Denomination or 12 + --- @type Balances + Balances = Balances or { [ao.id] = utils.toBalanceValue(0) } + --- @type TotalSupply + TotalSupply = TotalSupply or utils.toBalanceValue(0) + --- @type Name + Name = Name or "Points Coin" + --- @type Ticker + Ticker = Ticker or "PNTS" + --- @type Logo + Logo = Logo or "SBCCXwwecBlDqRLUjb8dYABExTJXLieawf7m2aBJ-KY" + --- @type MaxSupply + MaxSupply = MaxSupply or nil; + --- @type BondingCurveProcess + BondingCurveProcess = BondingCurveProcess or nil; + + -- Get token info + ---@type HandlerFunction + function mod.info(msg) + if msg.reply then + msg.reply({ + Action = 'Info-Response', + Name = Name, + Ticker = Ticker, + Logo = Logo, + Denomination = tostring(Denomination), + MaxSupply = MaxSupply, + TotalSupply = TotalSupply, + BondingCurveProcess = BondingCurveProcess, + }) + else + ao.send({ + Action = 'Info-Response', + Target = msg.From, + Name = Name, + Ticker = Ticker, + Logo = Logo, + Denomination = tostring(Denomination) + }) + end + end + + -- Get token total supply + ---@type HandlerFunction + function mod.totalSupply(msg) + assert(msg.From ~= ao.id, 'Cannot call Total-Supply from the same process!') + if msg.reply then + msg.reply({ + Action = 'Total-Supply-Response', + Data = TotalSupply, + Ticker = Ticker + }) + else + Send({ + Target = msg.From, + Action = 'Total-Supply-Response', + Data = TotalSupply, + Ticker = Ticker + }) + end + end + + -- Get token max supply + ---@type HandlerFunction + function mod.maxSupply(msg) + assert(msg.From ~= ao.id, 'Cannot call Max-Supply from the same process!') + + if msg.reply then + msg.reply({ + Action = 'Max-Supply-Response', + Data = MaxSupply, + Ticker = Ticker + }) + else + ao.send({ + Target = msg.From, + Action = 'Max-Supply-Response', + Data = MaxSupply, + Ticker = Ticker + }) + end + end + + return mod + +end + +_G.package.loaded["src.handlers.token"] = _loaded_mod_src_handlers_token() + +-- module: "src.libs.aolibs" +local function _loaded_mod_src_libs_aolibs() + -- These libs should exist in ao + + local mod = {} + + -- Define json + + local cjsonstatus, cjson = pcall(require, "cjson") + + if cjsonstatus then + mod.json = cjson + else + local jsonstatus, json = pcall(require, "json") + if not jsonstatus then + error("Library 'json' does not exist") + else + mod.json = json + end + end + + return mod +end + +_G.package.loaded["src.libs.aolibs"] = _loaded_mod_src_libs_aolibs() + +-- module: "src.handlers.balance" +local function _loaded_mod_src_handlers_balance() + local aolibs = require "src.libs.aolibs" + local json = aolibs.json + + local mod = {} + + -- Get target balance + ---@type HandlerFunction + function mod.balance(msg) + local bal = '0' + + -- If not Recipient is provided, then return the Senders balance + if (msg.Tags.Recipient) then + if (Balances[msg.Tags.Recipient]) then + bal = Balances[msg.Tags.Recipient] + end + elseif msg.Tags.Target and Balances[msg.Tags.Target] then + bal = Balances[msg.Tags.Target] + elseif Balances[msg.From] then + bal = Balances[msg.From] + end + if msg.reply then + msg.reply({ + Action = 'Balance-Response', + Balance = bal, + Ticker = Ticker, + Account = msg.Tags.Recipient or msg.From, + Data = bal + }) + else + ao.send({ + Action = 'Balance-Response', + Target = msg.From, + Balance = bal, + Ticker = Ticker, + Account = msg.Tags.Recipient or msg.From, + Data = bal + }) + end + end + + -- Get balances + ---@type HandlerFunction + function mod.balances(msg) + if msg.reply then + msg.reply({ Data = json.encode(Balances) }) + else + ao.send({ Target = msg.From, Data = json.encode(Balances) }) + end + end + + return mod + +end + +_G.package.loaded["src.handlers.balance"] = _loaded_mod_src_handlers_balance() + +-- module: "src.handlers.transfer" +local function _loaded_mod_src_handlers_transfer() + local bint = require('.bint')(256) + local utils = require "src.utils.mod" + + local mod = {} + + + function mod.transfer(msg) + assert(type(msg.Recipient) == 'string', 'Recipient is required!') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(bint.__lt(0, bint(msg.Quantity)), 'Quantity must be greater than 0') + + if not Balances[msg.From] then Balances[msg.From] = "0" end + if not Balances[msg.Recipient] then Balances[msg.Recipient] = "0" end + + if bint(msg.Quantity) <= bint(Balances[msg.From]) then + Balances[msg.From] = utils.subtract(Balances[msg.From], msg.Quantity) + Balances[msg.Recipient] = utils.add(Balances[msg.Recipient], msg.Quantity) + + --[[ + Only send the notifications to the Sender and Recipient + if the Cast tag is not set on the Transfer message + ]] + -- + if not msg.Cast then + -- Debit-Notice message template, that is sent to the Sender of the transfer + local debitNotice = { + Action = 'Debit-Notice', + Recipient = msg.Recipient, + Quantity = msg.Quantity, + Data = Colors.gray .. + "You transferred " .. + Colors.blue .. msg.Quantity .. Colors.gray .. " to " .. Colors.green .. msg.Recipient .. Colors.reset + } + -- Credit-Notice message template, that is sent to the Recipient of the transfer + local creditNotice = { + Target = msg.Recipient, + Action = 'Credit-Notice', + Sender = msg.From, + Quantity = msg.Quantity, + Data = Colors.gray .. + "You received " .. + Colors.blue .. msg.Quantity .. Colors.gray .. " from " .. Colors.green .. msg.From .. Colors.reset + } + + -- Add forwarded tags to the credit and debit notice messages + for tagName, tagValue in pairs(msg) do + -- Tags beginning with "X-" are forwarded + if string.sub(tagName, 1, 2) == "X-" then + debitNotice[tagName] = tagValue + creditNotice[tagName] = tagValue + end + end + + -- Send Debit-Notice and Credit-Notice + if msg.reply then + msg.reply(debitNotice) + else + debitNotice.Target = msg.From + Send(debitNotice) + end + Send(creditNotice) + end + else + if msg.reply then + msg.reply({ + Action = 'Transfer-Error', + ['Message-Id'] = msg.Id, + Error = 'Insufficient Balance!' + }) + else + Send({ + Target = msg.From, + Action = 'Transfer-Error', + ['Message-Id'] = msg.Id, + Error = 'Insufficient Balance!' + }) + end + end + end + + return mod + +end + +_G.package.loaded["src.handlers.transfer"] = _loaded_mod_src_handlers_transfer() + +-- module: "arweave.types.type" +local function _loaded_mod_arweave_types_type() + ---@class Type + local Type = { + -- custom name for the defined type + ---@type string|nil + name = nil, + -- list of assertions to perform on any given value + ---@type { message: string, validate: fun(val: any): boolean }[] + conditions = nil + } + + -- Execute an assertion for a given value + ---@param val any Value to assert for + ---@param message string? Optional message to throw + ---@param no_error boolean? Optionally disable error throwing (will return boolean) + function Type:assert(val, message, no_error) + for _, condition in ipairs(self.conditions) do + if not condition.validate(val) then + if no_error then + return false + end + self:error(message or condition.message) + end + end + + if no_error then + return true + end + end + + -- Add a custom condition/assertion to assert for + ---@param message string Error message for the assertion + ---@param assertion fun(val: any): boolean Custom assertion function that is asserted with the provided value + function Type:custom(message, assertion) + -- condition to add + local condition = { + message = message, + validate = assertion + } + + -- new instance if there are no conditions yet + if self.conditions == nil then + local instance = { + conditions = {} + } + + table.insert(instance.conditions, condition) + setmetatable(instance, self) + self.__index = self + + return instance + end + + table.insert(self.conditions, condition) + return self + end + + -- Add an assertion for built in types + ---@param t "nil"|"number"|"string"|"boolean"|"table"|"function"|"thread"|"userdata" Type to assert for + ---@param message string? Optional assertion error message + function Type:type(t, message) + return self:custom(message or ("Not of type (" .. t .. ")"), function(val) + return type(val) == t + end) + end + + -- Type must be userdata + ---@param message string? Optional assertion error message + function Type:userdata(message) + return self:type("userdata", message) + end + + -- Type must be thread + ---@param message string? Optional assertion error message + function Type:thread(message) + return self:type("thread", message) + end + + -- Type must be table + ---@param message string? Optional assertion error message + function Type:table(message) + return self:type("table", message) + end + + -- Table's keys must be of type t + ---@param t Type Type to assert the keys for + ---@param message string? Optional assertion error message + function Type:keys(t, message) + return self:custom(message or "Invalid table keys", function(val) + if type(val) ~= "table" then + return false + end + + for key, _ in pairs(val) do + -- check if the assertion throws any errors + local success = pcall(function() + return t:assert(key) + end) + + if not success then + return false + end + end + + return true + end) + end + + -- Type must be array + ---@param message string? Optional assertion error message + function Type:array(message) + return self:table():keys(Type:number(), message) + end + + -- Table's values must be of type t + ---@param t Type Type to assert the values for + ---@param message string? Optional assertion error message + function Type:values(t, message) + return self:custom(message or "Invalid table values", function(val) + if type(val) ~= "table" then + return false + end + + for _, v in pairs(val) do + -- check if the assertion throws any errors + local success = pcall(function() + return t:assert(v) + end) + + if not success then + return false + end + end + + return true + end) + end + + -- Type must be boolean + ---@param message string? Optional assertion error message + function Type:boolean(message) + return self:type("boolean", message) + end + + -- Type must be function + ---@param message string? Optional assertion error message + function Type:_function(message) + return self:type("function", message) + end + + -- Type must be nil + ---@param message string? Optional assertion error message + function Type:_nil(message) + return self:type("nil", message) + end + + -- Value must be the same + ---@param val any The value the assertion must be made with + ---@param message string? Optional assertion error message + function Type:is(val, message) + return self:custom(message + or "Value did not match expected value (Type:is(expected))", + function(v) + return v == val + end) + end + + -- Type must be string + ---@param message string? Optional assertion error message + function Type:string(message) + return self:type("string", message) + end + + -- String type must match pattern + ---@param pattern string Pattern to match + ---@param message string? Optional assertion error message + function Type:match(pattern, message) + return self:custom(message + or ("String did not match pattern \"" .. pattern .. "\""), + function(val) + return string.match(val, pattern) ~= nil + end) + end + + -- String type must be of defined length + ---@param len number Required length + ---@param match_type? "less"|"greater" String length should be "less" than or "greater" than the defined length. Leave empty for exact match. + ---@param message string? Optional assertion error message + function Type:length(len, match_type, message) + local match_msgs = { + less = "String length is not less than " .. len, + greater = "String length is not greater than " .. len, + default = "String is not of length " .. len + } + + return self:custom(message or (match_msgs[match_type] or match_msgs.default), + function(val) + local strlen = string.len(val) + + -- validate length + if match_type == "less" then + return strlen < len + elseif match_type == "greater" then + return strlen > len + end + + return strlen == len + end) + end + + -- Type must be a number + ---@param message string? Optional assertion error message + function Type:number(message) + return self:type("number", message) + end + + -- Number must be an integer (chain after "number()") + ---@param message string? Optional assertion error message + function Type:integer(message) + return self:custom(message or "Number is not an integer", function(val) + return val % 1 == 0 + end) + end + + -- Number must be even (chain after "number()") + ---@param message string? Optional assertion error message + function Type:even(message) + return self:custom(message or "Number is not even", function(val) + return val % 2 == 0 + end) + end + + -- Number must be odd (chain after "number()") + ---@param message string? Optional assertion error message + function Type:odd(message) + return self:custom(message or "Number is not odd", function(val) + return val % 2 == 1 + end) + end + + -- Number must be less than the number "n" (chain after "number()") + ---@param n number Number to compare with + ---@param message string? Optional assertion error message + function Type:less_than(n, message) + return self:custom(message or ("Number is not less than " .. n), function(val) + return val < n + end) + end + + -- Number must be greater than the number "n" (chain after "number()") + ---@param n number Number to compare with + ---@param message string? Optional assertion error message + function Type:greater_than(n, message) + return self:custom(message or ("Number is not greater than" .. n), + function(val) + return val > n + end) + end + + -- Make a type optional (allow them to be nil apart from the required type) + ---@param t Type Type to assert for if the value is not nil + ---@param message string? Optional assertion error message + function Type:optional(t, message) + return self:custom(message or "Optional type did not match", function(val) + if val == nil then + return true + end + + t:assert(val) + return true + end) + end + + -- Table must be of object + ---@param obj { [any]: Type } + ---@param strict? boolean Only allow the defined keys from the object, throw error on other keys (false by default) + ---@param message string? Optional assertion error message + function Type:object(obj, strict, message) + if type(obj) ~= "table" then + self:error( + "Invalid object structure provided for object assertion (has to be a table):\n" + .. tostring(obj)) + end + + return self:custom(message + or ("Not of defined object (" .. tostring(obj) .. ")"), + function(val) + if type(val) ~= "table" then + return false + end + + -- for each value, validate + for key, assertion in pairs(obj) do + if val[key] == nil then + return false + end + + -- check if the assertion throws any errors + local success = pcall(function() + return assertion:assert(val[key]) + end) + + if not success then + return false + end + end + + -- in strict mode, we do not allow any other keys + if strict then + for key, _ in pairs(val) do + if obj[key] == nil then + return false + end + end + end + + return true + end) + end + + -- Type has to be either one of the defined assertions + ---@param ... Type Type(s) to assert for + function Type:either(...) + ---@type Type[] + local assertions = { + ... + } + + return self:custom("Neither types matched defined in (Type:either(...))", + function(val) + for _, assertion in ipairs(assertions) do + if pcall(function() + return assertion:assert(val) + end) then + return true + end + end + + return false + end) + end + + -- Type cannot be the defined assertion (tip: for multiple negated assertions, use Type:either(...)) + ---@param t Type Type to NOT assert for + ---@param message string? Optional assertion error message + function Type:is_not(t, message) + return self:custom(message + or "Value incorrectly matched with the assertion provided (Type:is_not())", + function(val) + local success = pcall(function() + return t:assert(val) + end) + + return not success + end) + end + + -- Set the name of the custom type + -- This will be used with error logs + ---@param name string Name of the type definition + function Type:set_name(name) + self.name = name + return self + end + + -- Throw an error + ---@param message any Message to log + ---@private + function Type:error(message) + error("[Type " .. (self.name or tostring(self.__index)) .. "] " + .. tostring(message)) + end + + return Type + +end + +_G.package.loaded["arweave.types.type"] = _loaded_mod_arweave_types_type() + +-- module: "src.utils.assertions" +local function _loaded_mod_src_utils_assertions() + local Type = require "arweave.types.type" + + local mod = {} + + ---Assert value is an Arweave address + ---@param name string + ---@param value string + mod.isAddress = function(name, value) + Type + :string("Invalid type for `" .. name .. "`. Expected a string for Arweave address.") + :length(43, nil, "Incorrect length for Arweave address `" .. name .. "`. Must be exactly 43 characters long.") + :match("[a-zA-Z0-9-_]+", + "Invalid characters in Arweave address `" .. + name .. "`. Only alphanumeric characters, dashes, and underscores are allowed.") + :assert(value) + end + + ---Assert value is an UUID + ---@param name string + ---@param value string + mod.isUuid = function(name, value) + Type + :string("Invalid type for `" .. name .. "`. Expected a string for UUID.") + :match("^[0-9a-fA-F]%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$", + "Invalid UUID format for `" .. name .. "`. A valid UUID should follow the 8-4-4-4-12 hexadecimal format.") + :assert(value) + end + + mod.Array = Type:array("Invalid type (must be array)") + + -- string assertion + mod.String = Type:string("Invalid type (must be a string)") + + -- Assert not empty string + ---@param value any Value to assert for + ---@param message string? Optional message to throw + ---@param len number Required length + ---@param match_type? "less"|"greater" String length should be "less" than or "greater" than the defined length. Leave empty for exact match. + ---@param len_message string? Optional assertion error message for length + mod.assertNotEmptyString = function(value, message, len, match_type, len_message) + Type:string(message):length(len, match_type, len_message):assert(value) + end + + -- number assertion + mod.Integer = Type:number():integer("Invalid type (must be a integer)") + -- number assertion + mod.Number = Type:number("Invalid type (must be a number)") + + -- repo name assertion + mod.RepoName = Type + :string("Invalid type for Repository name (must be a string)") + :match("^[a-zA-Z0-9._-]+$", + "The repository name can only contain ASCII letters, digits, and the characters ., -, and _") + + return mod + +end + +_G.package.loaded["src.utils.assertions"] = _loaded_mod_src_utils_assertions() + +-- module: "src.handlers.mint" +local function _loaded_mod_src_handlers_mint() + local bint = require('.bint')(256) + local utils = require "src.utils.mod" + local assertions = require "src.utils.assertions" + local mod = {} + + function mod.mint(msg) + assert(msg.From == BondingCurveProcess, 'Only the bonding curve process can mint!') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(bint.__lt(0, bint(msg.Quantity)), 'Quantity must be greater than zero!') + + -- Check if minting would exceed max supply + local newTotalSupply = utils.add(TotalSupply, msg.Quantity) + + if bint.__lt(bint(MaxSupply), bint(newTotalSupply)) then + msg.reply({ + Action = 'Mint-Error', + Error = 'Minting would exceed max supply!' + }) + + return + end + + -- Calculate required reserve amount + local recipient = msg.Tags.Recipient or msg.From + + assertions.isAddress("Recipient", recipient) + + -- Update balances + if not Balances[recipient] then Balances[recipient] = "0" end + + Balances[recipient] = utils.add(Balances[recipient], msg.Quantity) + TotalSupply = utils.add(TotalSupply, msg.Quantity) + + if msg.reply then + msg.reply({ + Action = 'Mint-Response', + Data = "Successfully minted " .. msg.Quantity + }) + else + ao.send({ + Action = 'Mint-Response', + Target = msg.From, + Data = "Successfully minted " .. msg.Quantity + }) + end + end + + return mod + +end + +_G.package.loaded["src.handlers.mint"] = _loaded_mod_src_handlers_mint() + +-- module: "src.handlers.burn" +local function _loaded_mod_src_handlers_burn() + local bint = require('.bint')(256) + local utils = require "src.utils.mod" + local assertions = require "src.utils.assertions" + local mod = {} + + function mod.burn(msg) + assert(msg.From == BondingCurveProcess, 'Only the bonding curve process can burn!') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + + local user = msg.Tags.Recipient + assertions.isAddress("Recipient", user) + + if bint.__lt(bint(Balances[user]), bint(msg.Quantity)) then + msg.reply({ + Action = 'Burn-Error', + Error = 'Quantity must be less than or equal to the current balance' + }) + + return + end + + -- Update balances + Balances[user] = utils.subtract(Balances[user], msg.Quantity) + TotalSupply = utils.subtract(TotalSupply, msg.Quantity) + + + + if msg.reply then + msg.reply({ + Action = 'Burn-Response', + Data = "Successfully burned " .. msg.Quantity + }) + else + ao.send({ + Action = 'Burn-Response', + + Target = msg.From, + Data = "Successfully burned " .. msg.Quantity + }) + end + end + + return mod + +end + +_G.package.loaded["src.handlers.burn"] = _loaded_mod_src_handlers_burn() + +local token = require "src.handlers.token" +local balance = require "src.handlers.balance" +local transfer = require "src.handlers.transfer" +local mint = require "src.handlers.mint" +local burn = require "src.handlers.burn" + +-- Info +Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), token.info) + +-- Total Supply +Handlers.add('Total-Supply', Handlers.utils.hasMatchingTag('Action', "Total-Supply"), token.totalSupply) + +-- Max Supply +Handlers.add('Max-Supply', Handlers.utils.hasMatchingTag('Action', "Max-Supply"), token.maxSupply) + +-- Balance +Handlers.add('Balance', Handlers.utils.hasMatchingTag('Action', 'Balance'), balance.balance) + +-- Balances +Handlers.add('Balances', Handlers.utils.hasMatchingTag('Action', 'Balances'), balance.balances) + +-- Transfer +Handlers.add('Transfer', Handlers.utils.hasMatchingTag('Action', 'Transfer'), transfer.transfer) + +-- Mint +Handlers.add('Mint', Handlers.utils.hasMatchingTag('Action', 'Mint'), mint.mint) + +-- Burn +Handlers.add('Burn', Handlers.utils.hasMatchingTag('Action', 'Burn'), burn.burn) \ No newline at end of file diff --git a/ao-contracts/curve-bonded-token/src/contracts/curve_bonded_token.lua b/ao-contracts/curve-bonded-token/src/contracts/curve_bonded_token.lua new file mode 100644 index 00000000..9da3b76e --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/contracts/curve_bonded_token.lua @@ -0,0 +1,29 @@ +local token = require "src.handlers.token" +local balance = require "src.handlers.balance" +local transfer = require "src.handlers.transfer" +local mint = require "src.handlers.mint" +local burn = require "src.handlers.burn" + +-- Info +Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), token.info) + +-- Total Supply +Handlers.add('Total-Supply', Handlers.utils.hasMatchingTag('Action', "Total-Supply"), token.totalSupply) + +-- Max Supply +Handlers.add('Max-Supply', Handlers.utils.hasMatchingTag('Action', "Max-Supply"), token.maxSupply) + +-- Balance +Handlers.add('Balance', Handlers.utils.hasMatchingTag('Action', 'Balance'), balance.balance) + +-- Balances +Handlers.add('Balances', Handlers.utils.hasMatchingTag('Action', 'Balances'), balance.balances) + +-- Transfer +Handlers.add('Transfer', Handlers.utils.hasMatchingTag('Action', 'Transfer'), transfer.transfer) + +-- Mint +Handlers.add('Mint', Handlers.utils.hasMatchingTag('Action', 'Mint'), mint.mint) + +-- Burn +Handlers.add('Burn', Handlers.utils.hasMatchingTag('Action', 'Burn'), burn.burn) diff --git a/ao-contracts/curve-bonded-token/src/contracts/curve_bonded_token_manager.lua b/ao-contracts/curve-bonded-token/src/contracts/curve_bonded_token_manager.lua new file mode 100644 index 00000000..29effebd --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/contracts/curve_bonded_token_manager.lua @@ -0,0 +1,27 @@ +local bondingCurve = require "src.handlers.bonding_curve" +local tokenManager = require "src.handlers.token_manager" +local liquidityPool = require "src.handlers.liquidity_pool" + +Handlers.add('Initialize-Bonding-Curve', Handlers.utils.hasMatchingTag('Action', 'Initialize-Bonding-Curve'), + tokenManager.initialize) + +Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), + tokenManager.info) + +Handlers.add('Get-Buy-Price', Handlers.utils.hasMatchingTag('Action', 'Get-Buy-Price'), + bondingCurve.getBuyPrice) + +Handlers.add('Get-Sell-Price', Handlers.utils.hasMatchingTag('Action', 'Get-Sell-Price'), + bondingCurve.getSellPrice) + +Handlers.add('Sell-Tokens', Handlers.utils.hasMatchingTag('Action', 'Sell-Tokens'), + bondingCurve.sellTokens) + + +Handlers.add('Deposit-To-Liquidity-Pool', Handlers.utils.hasMatchingTag('Action', 'Deposit-To-Liquidity-Pool'), + liquidityPool.depositToLiquidityPool) + +Handlers.add( + "Buy-Tokens", + { Action = "Credit-Notice", ["X-Action"] = "Buy-Tokens" }, + bondingCurve.buyTokens) diff --git a/ao-contracts/curve-bonded-token/src/handlers/balance.lua b/ao-contracts/curve-bonded-token/src/handlers/balance.lua new file mode 100644 index 00000000..e6f92319 --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/handlers/balance.lua @@ -0,0 +1,51 @@ +local aolibs = require "src.libs.aolibs" +local json = aolibs.json + +local mod = {} + +-- Get target balance +---@type HandlerFunction +function mod.balance(msg) + local bal = '0' + + -- If not Recipient is provided, then return the Senders balance + if (msg.Tags.Recipient) then + if (Balances[msg.Tags.Recipient]) then + bal = Balances[msg.Tags.Recipient] + end + elseif msg.Tags.Target and Balances[msg.Tags.Target] then + bal = Balances[msg.Tags.Target] + elseif Balances[msg.From] then + bal = Balances[msg.From] + end + if msg.reply then + msg.reply({ + Action = 'Balance-Response', + Balance = bal, + Ticker = Ticker, + Account = msg.Tags.Recipient or msg.From, + Data = bal + }) + else + ao.send({ + Action = 'Balance-Response', + Target = msg.From, + Balance = bal, + Ticker = Ticker, + Account = msg.Tags.Recipient or msg.From, + Data = bal + }) + end +end + +-- Get balances +---@type HandlerFunction +function mod.balances(msg) + if msg.reply then + msg.reply({ Data = json.encode(Balances) }) + else + ao.send({ Target = msg.From, Data = json.encode(Balances) }) + end +end + +return mod diff --git a/ao-contracts/curve-bonded-token/src/handlers/bonding_curve.lua b/ao-contracts/curve-bonded-token/src/handlers/bonding_curve.lua new file mode 100644 index 00000000..b9531357 --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/handlers/bonding_curve.lua @@ -0,0 +1,591 @@ +local utils = require "src.utils.mod" +local aolibs = require "src.libs.aolibs" +local bint = require('.bint')(256) +local json = aolibs.json + +local mod = {} + +--[[ +--- Get price for purchasing tokenQuantity of tokens +--]] +local EXP_N = 2 -- exponent determining the rate of increase +local EXP_N_PLUS1 = EXP_N + 1 + +--- @type table +RefundsMap = {} + + +function GetCurrentSupply() + Send({ Target = RepoToken.processId, Action = "Total-Supply" }) + local currentSupplyResp = Receive( + function(m) + return m.Tags['From-Process'] == RepoToken.processId and + m.Tags['Action'] == 'Total-Supply-Response' + end) + + return currentSupplyResp.Data +end + +function RefundHandler(amount, target, pid) + local refundResp = ao.send({ + Target = pid, + Action = "Transfer", + Recipient = target, + Quantity = tostring(amount), + }).receive() + + if refundResp.Tags['Action'] ~= 'Debit-Notice' then + RefundsMap[target] = tostring(amount) + msg.reply({ + Action = 'Refund-Error', + Error = 'Refund failed' + }) + + return false + end + + return true +end + +function LogActivity(action, data, msg) + ao.send({ Target = ao.id, Action = "Activity-Log", ["User-Action"] = action, Data = data, Message = msg }) +end + +---@type HandlerFunction +function mod.getBuyPrice(msg) + local tokensToBuy = msg.Tags['Token-Quantity'] + local currentSupply = msg.Tags['Current-Supply'] + assert(type(tokensToBuy) == 'string', 'Token quantity is required!') + + local tokensToBuyInSubUnits = utils.toSubUnits(tokensToBuy, RepoToken.denomination) + assert(bint.__lt(0, tokensToBuyInSubUnits), 'Token quantity must be greater than zero!') + + if (Initialized ~= true or RepoToken == nil or RepoToken.processId == nil) then + msg.reply({ + Action = 'Get-Buy-Price-Error', + Error = 'Bonding curve not initialized' + }) + + return + end + if (currentSupply == nil) then + msg.reply({ + Action = 'Get-Buy-Price-Error', + Error = 'Failed to get current supply of curve bonded token' + }) + + return + end + + -- current supply is returned in sub units + -- local preAllocation = utils.add(AllocationForLP, AllocationForCreator) + local s1 = currentSupply + local s2 = utils.add(currentSupply, tokensToBuyInSubUnits); + + if bint.__lt(bint(SupplyToSell), bint(s2)) then + local diff = utils.subtract(SupplyToSell, currentSupply) + msg.reply({ + Action = 'Get-Buy-Price-Error', + Error = 'Not enough tokens to sell. Remaining: ' .. diff + }) + + return + end + + local S_exp = bint.ipow(bint(SupplyToSell), bint(EXP_N_PLUS1)) + + if bint.__le(S_exp, 0) then + msg.reply({ + Action = 'Get-Buy-Price-Error', + Error = 'Bonding curve error: S_EXP too low ' .. S_exp + }) + + return + end + + -- Cost = G * [ (s2)^(n+1) - (s1)^(n+1) ] / S^(n+1) + local s1_exp = bint.ipow(bint(s1), bint(EXP_N_PLUS1)) + local s2_exp = bint.ipow(bint(s2), bint(EXP_N_PLUS1)) + + local numerator = utils.multiply(FundingGoal, utils.subtract(s2_exp, s1_exp)) + local cost = utils.divide(numerator, S_exp) + + local roundedCost = math.ceil(utils.toNumber(cost)) + + msg.reply({ + Action = 'Get-Buy-Price-Response', + Price = utils.toBalanceValue(roundedCost), + CurrentSupply = currentSupply, + TokensToBuy = tokensToBuy, + Data = utils.toBalanceValue(roundedCost), + Denomination = ReserveToken.denomination, + Ticker = ReserveToken.tokenTicker, + RawPrice = cost + }) +end + +--[[ +--- Get price for selling tokenQuantity of tokens +--- +--]] +---@type HandlerFunction +function mod.getSellPrice(msg) + local tokensToSell = msg.Tags['Token-Quantity'] + assert(type(tokensToSell) == 'string', 'Token quantity is required!') + + if bint.__le(bint(ReserveBalance), 0) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'No reserve balance to sell!' + }) + + return + end + + local tokensToSellInSubUnits = utils.toSubUnits(tokensToSell, RepoToken.denomination) + assert(bint.__lt(0, tokensToSellInSubUnits), 'Token quantity must be greater than zero!') + + if (Initialized ~= true or RepoToken == nil or RepoToken.processId == nil) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'Bonding curve not initialized' + }) + + return + end + + ao.send({ Target = RepoToken.processId, Action = "Total-Supply" }) + local currentSupplyResp = Receive( + function(m) + return m.Tags['From-Process'] == RepoToken.processId and + m.Data ~= nil + end) + + if (currentSupplyResp == nil or currentSupplyResp.Data == nil) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'Failed to get current supply of curve bonded token' + }) + + return + end + + -- current supply is returned in sub units + local preAllocation = utils.add(AllocationForLP, AllocationForCreator) + local s1 = utils.subtract(currentSupplyResp.Data, preAllocation) + local s2 = utils.subtract(s1, tokensToSellInSubUnits); + + local S_exp = bint.ipow(bint(SupplyToSell), bint(EXP_N_PLUS1)) + + if bint.__le(S_exp, 0) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'Bonding curve error: S_EXP too low ' .. S_exp + }) + + return + end + + -- Cost = G * [ (s2)^(n+1) - (s1)^(n+1) ] / S^(n+1) + local s1_exp = bint.ipow(bint(s1), bint(EXP_N_PLUS1)) + local s2_exp = bint.ipow(bint(s2), bint(EXP_N_PLUS1)) + + local numerator = utils.multiply(FundingGoal, utils.subtract(s2_exp, s1_exp)) + local cost = utils.divide(numerator, S_exp) + + if bint.__lt(bint(cost), bint(ReserveBalance)) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'Bonding curve error: Insufficient reserve balance to sell: ' .. cost + }) + + return + end + + msg.reply({ + Action = 'Get-Sell-Price-Response', + Price = cost, + Data = cost, + Denomination = ReserveToken.denomination, + Ticker = ReserveToken.tokenTicker + }) +end + +---@type HandlerFunction +function mod.buyTokens(msg, env) + LogActivity(msg.Tags['X-Action'], json.encode(msg.Tags), "Buy-Tokens Called") + local reservePID = msg.From + + local sender = msg.Tags.Sender + + local quantityReservesSent = bint(msg.Tags.Quantity) + + -- local slippageTolerance = tonumber(msg.Tags["X-Slippage-Tolerance"]) or 0 + + assert(ReachedFundingGoal == false, 'Funding goal has been reached!') + + local tokensToBuy = msg.Tags['X-Token-Quantity'] + assert(type(tokensToBuy) == 'string', 'Token quantity is required!') + + local tokensToBuyInSubUnits = utils.toSubUnits(tokensToBuy, RepoToken.denomination) + assert(bint.__lt(0, tokensToBuyInSubUnits), 'Token quantity must be greater than zero!') + + if (Initialized ~= true or RepoToken == nil or RepoToken.processId == nil) then + msg.reply({ + Action = 'Buy-Tokens-Error', + Error = 'Bonding curve not initialized' + }) + + return + end + + -- double call issue + local currentSupplyResp = ao.send({ Target = RepoToken.processId, Action = "Total-Supply" }).receive().Data + -- local currentSupplyResp = msg.Tags['X-Current-Supply'] + if (currentSupplyResp == nil) then + msg.reply({ + Action = 'Buy-Tokens-Error', + Error = 'Failed to get current supply of curve bonded token' + }) + + return + end + + currentSupplyResp = tostring(currentSupplyResp) + + -- current supply is returned in sub units + -- local preAllocation = utils.add(AllocationForLP, AllocationForCreator) + local s1 = currentSupplyResp + local s2 = utils.add(currentSupplyResp, tokensToBuyInSubUnits); + -- Calculate remaining tokens + local remainingTokens = utils.subtract(SupplyToSell, currentSupplyResp) + + -- Check if there are enough tokens to sell + if bint.__lt(bint(remainingTokens), bint(tokensToBuyInSubUnits)) then + msg.reply({ + Action = 'Buy-Tokens-Error', + Remaining = tostring(remainingTokens), + TokensToBuy = tostring(tokensToBuyInSubUnits), + Error = 'Not enough tokens to sell.' + }) + return + end + + local S_exp = bint.ipow(bint(SupplyToSell), bint(EXP_N_PLUS1)) + + if bint.__le(S_exp, 0) then + msg.reply({ + Action = 'Buy-Tokens-Error', + Error = 'Bonding curve error: S_EXP too low ' .. S_exp + }) + + return + end + + -- Cost = G * [ (s2)^(n+1) - (s1)^(n+1) ] / S^(n+1) + local s1_exp = bint.ipow(bint(s1), bint(EXP_N_PLUS1)) + local s2_exp = bint.ipow(bint(s2), bint(EXP_N_PLUS1)) + + local numerator = utils.multiply(FundingGoal, utils.subtract(s2_exp, s1_exp)) + local cost = utils.divide((numerator), S_exp) + LogActivity(msg.Tags['X-Action'], json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Calculated cost of buying tokens for Reserves sent") + if bint.__lt(bint(quantityReservesSent), bint(math.ceil(utils.toNumber(cost)))) then + LogActivity(msg.Tags['X-Action'], + json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Insufficient funds sent to buy") + local refundSuccess = RefundHandler(quantityReservesSent, sender, reservePID) + + if not refundSuccess then + LogActivity(msg.Tags['X-Action'], + json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Refund failed") + return + end + + ao.send({ + Target = sender, + Action = "Refund-Notice", + Quantity = tostring(quantityReservesSent), + }) + + msg.reply({ + Cost = tostring(cost), + AmountSent = tostring(quantityReservesSent), + Action = 'Buy-Tokens-Error', + Error = 'Insufficient funds sent to buy' + }) + + return + end + + local mintResp = ao.send({ Target = RepoToken.processId, Action = "Mint", Tags = { Quantity = utils.toBalanceValue(tokensToBuyInSubUnits), Recipient = sender } }) + .receive() + + if mintResp.Tags['Action'] ~= 'Mint-Response' then + LogActivity(msg.Tags['X-Action'], + json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Failed to mint tokens") + local refundSuccess = RefundHandler(quantityReservesSent, sender, reservePID) + + if not refundSuccess then + LogActivity(msg.Tags['X-Action'], + json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Refund failed after failed mint") + return + end + + ao.send({ + Target = sender, + Action = "Refund-Notice", + Quantity = tostring(quantityReservesSent), + }) + msg.reply({ + Action = 'Buy-Tokens-Error', + Error = 'Failed to mint tokens. Amount will be refunded.' + }) + + return + end + + ReserveBalance = utils.add(ReserveBalance, quantityReservesSent) + if bint(ReserveBalance) >= bint(FundingGoal) then + ReachedFundingGoal = true + end + + LogActivity(msg.Tags['X-Action'], json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Successfully bought tokens") + + msg.reply({ + Action = 'Buy-Tokens-Response', + TokensBought = utils.toBalanceValue(tokensToBuyInSubUnits), + Cost = tostring(cost), + Data = mintResp.Data or ('Successfully bought ' .. tokensToBuy .. ' tokens') + }) +end + +---@type HandlerFunction +function mod.sellTokens(msg) + LogActivity(msg.Tags['Action'], json.encode(msg.Tags), "Sell-Tokens Called") + assert(ReachedFundingGoal == false, 'Funding goal has been reached!') + + local tokensToSell = msg.Tags['Token-Quantity'] + assert(type(tokensToSell) == 'string', 'Token quantity is required!') + + if bint.__le(bint(ReserveBalance), 0) then + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'No reserve balance to sell!' + }) + + return + end + + local tokensToSellInSubUnits = utils.toSubUnits(tokensToSell, RepoToken.denomination) + assert(bint.__lt(0, tokensToSellInSubUnits), 'Token quantity must be greater than zero!') + + if (Initialized ~= true or RepoToken == nil or RepoToken.processId == nil) then + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Bonding curve not initialized' + }) + + return + end + + local currentSupplyResp = ao.send({ Target = RepoToken.processId, Action = "Total-Supply" }).receive() + if (currentSupplyResp == nil or currentSupplyResp.Data == nil) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'Failed to get current supply of curve bonded token' + }) + + return + end + + if bint.__le(bint(currentSupplyResp.Data), 0) then + LogActivity(msg.Tags['Action'], + json.encode({ CurrentSupply = currentSupplyResp.Data, TokensToSell = tokensToSell }), + "No tokens to sell. Buy some tokens first.") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'No tokens to sell. Buy some tokens first.' + }) + + return + end + + -- Check if there are enough tokens to sell + if bint.__lt(bint(currentSupplyResp.Data), bint(tokensToSellInSubUnits)) then + LogActivity(msg.Tags['Action'], + json.encode({ CurrentSupply = currentSupplyResp.Data, TokensToSell = tostring(tokensToSellInSubUnits) }), + "Not enough tokens to sell.") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Not enough tokens to sell.' + }) + return + end + + -- current supply is returned in sub units + -- local preAllocation = utils.add(AllocationForLP, AllocationForCreator) + local s1 = currentSupplyResp.Data + local s2 = utils.subtract(currentSupplyResp.Data, tokensToSellInSubUnits); + + local S_exp = bint.ipow(bint(SupplyToSell), bint(EXP_N_PLUS1)) + + if bint.__le(S_exp, 0) then + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Bonding curve error: S_EXP too low ' .. S_exp + }) + + return + end + + -- Cost = G * [ (s2)^(n+1) - (s1)^(n+1) ] / S^(n+1) + local s1_exp = bint.ipow(bint(s1), bint(EXP_N_PLUS1)) + local s2_exp = bint.ipow(bint(s2), bint(EXP_N_PLUS1)) + + local numerator = utils.multiply(FundingGoal, utils.subtract(s1_exp, s2_exp)) + local cost = utils.divide(numerator, S_exp) + + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits) + }), "Calculated cost of selling tokens") + + local balanceResp = ao.send({ Target = ReserveToken.processId, Action = "Balance" }).receive() + if balanceResp == nil or balanceResp.Data == nil then + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits) + }), + "Failed to get balance of reserve token") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Failed to get balance of reserve token' + }) + + return + end + LogActivity(msg.Tags['Action'], json.encode({ Balance = balanceResp.Data }), "Got balance of reserve token") + + if bint.__lt(bint(balanceResp.Data), bint(ReserveBalance)) then + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits), + Balance = balanceResp.Data, + ReserveBalance = ReserveBalance + }), + "Insufficient reserve balance to sell") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Insufficient reserve balance to sell', + ReserveBalance = tostring(ReserveBalance), + CurrentBalance = tostring(balanceResp.Data) + }) + + return + end + + if bint.__lt(bint(ReserveBalance), bint(cost)) then + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits), + Balance = balanceResp.Data, + ReserveBalance = ReserveBalance + }), + "Insufficient reserve balance to sell") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Insufficient reserve balance to sell', + ReserveBalance = tostring(ReserveBalance), + Cost = tostring(cost) + }) + + return + end + + local burnResp = ao.send({ Target = RepoToken.processId, Action = "Burn", Tags = { Quantity = tostring(tokensToSellInSubUnits), Recipient = msg.From } }) + .receive() + if burnResp.Tags['Action'] ~= 'Burn-Response' then + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits), + Balance = balanceResp.Data, + ReserveBalance = ReserveBalance + }), + "Failed to burn tokens") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Failed to burn tokens. Amount will be refunded.' + }) + + return + end + + local transferResp = ao.send({ + Target = ReserveToken.processId, + Action = "Transfer", + Recipient = msg.From, + Quantity = + tostring(cost) + }).receive() + if transferResp.Tags['Action'] ~= 'Debit-Notice' then + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits), + Balance = balanceResp.Data, + ReserveBalance = ReserveBalance + }), + "Failed to transfer reserve tokens") + RefundsMap[msg.From] = tostring(cost) + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Failed to transfer reserve tokens. try again.' + }) + + return + end + + ReserveBalance = utils.subtract(ReserveBalance, cost) + + + + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring(tokensToSellInSubUnits), + Balance = balanceResp.Data, + ReserveBalance = ReserveBalance + }), + "Successfully sold tokens") + msg.reply({ + Action = 'Sell-Tokens-Response', + TokensSold = utils.toBalanceValue(tokensToSellInSubUnits), + Cost = tostring(cost), + Data = 'Successfully sold ' .. tokensToSell .. ' tokens' + }) +end + +return mod diff --git a/ao-contracts/curve-bonded-token/src/handlers/burn.lua b/ao-contracts/curve-bonded-token/src/handlers/burn.lua new file mode 100644 index 00000000..4e24fc2a --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/handlers/burn.lua @@ -0,0 +1,43 @@ +local bint = require('.bint')(256) +local utils = require "src.utils.mod" +local assertions = require "src.utils.assertions" +local mod = {} + +function mod.burn(msg) + assert(msg.From == BondingCurveProcess, 'Only the bonding curve process can burn!') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + + local user = msg.Tags.Recipient + assertions.isAddress("Recipient", user) + + if bint.__lt(bint(Balances[user]), bint(msg.Quantity)) then + msg.reply({ + Action = 'Burn-Error', + Error = 'Quantity must be less than or equal to the current balance' + }) + + return + end + + -- Update balances + Balances[user] = utils.subtract(Balances[user], msg.Quantity) + TotalSupply = utils.subtract(TotalSupply, msg.Quantity) + + + + if msg.reply then + msg.reply({ + Action = 'Burn-Response', + Data = "Successfully burned " .. msg.Quantity + }) + else + ao.send({ + Action = 'Burn-Response', + + Target = msg.From, + Data = "Successfully burned " .. msg.Quantity + }) + end +end + +return mod diff --git a/ao-contracts/curve-bonded-token/src/handlers/liquidity_pool.lua b/ao-contracts/curve-bonded-token/src/handlers/liquidity_pool.lua new file mode 100644 index 00000000..44b1b6ee --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/handlers/liquidity_pool.lua @@ -0,0 +1,101 @@ +local utils = require('src.utils.mod') +local mod = {} + +function mod.depositToLiquidityPool(msg) + assert(msg.From == Creator, "Only the creator can make liquidity pool requests") + assert(LiquidityPool == nil, "Liquidity pool already initialized") + assert(ReachedFundingGoal == true, "Funding goal not reached") + + local poolId = msg.Tags['Pool-Id'] + assert(type(poolId) == 'string', "Pool ID is required") + + -- local mintQty = utils.divide(utils.multiply(MaxSupply, "20"), "100") + local mintResponse = ao.send({ + Target = RepoToken.processId, Action = "Mint", Quantity = AllocationForLP + }).receive() + + if mintResponse.Tags['Action'] ~= 'Mint-Response' then + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Error', + Error = 'Failed to mint tokens.' + }) + + return + end + + local balanceResponseRepoToken = ao.send({ + Target = RepoToken.processId, Action = "Balance" + }).receive() + + local tokenAQty = balanceResponseRepoToken.Data + + if tokenAQty == nil or tokenAQty == "0" then + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Error', + Error = "No repo tokens to deposit", + }) + return + end + + local balanceResponseReserveToken = ao.send({ + Target = ReserveToken.processId, Action = "Balance" + }).receive() + + local tokenBQty = balanceResponseReserveToken.Data + + if tokenBQty == nil or tokenBQty == "0" then + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Error', + Error = "No reserve tokens to deposit", + }) + return + end + + local tokenADepositResponse = ao.send({ + Target = RepoToken.processId, + Action = "Transfer", + Quantity = tokenAQty, + Recipient = poolId, + ["X-Action"] = "Provide", + ["X-Slippage-Tolerance"] = "0.5" + }).receive() + + if tokenADepositResponse.Tags['Action'] ~= 'Debit-Notice' then + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Error', + Error = 'Failed to transfer Repo tokens to LP. try again.' + }) + + return + end + + local tokenBDepositResponse = ao.send({ + Target = ReserveToken.processId, + Action = "Transfer", + Quantity = tokenBQty, + Recipient = poolId, + ["X-Action"] = "Provide", + ["X-Slippage-Tolerance"] = "0.5" + }).receive() + + if tokenBDepositResponse.Tags['Action'] ~= 'Debit-Notice' then + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Error', + Error = 'Failed to transfer Repo tokens to LP. try again.' + }) + + return + end + + LiquidityPool = poolId + + --Check reserves of the pool + + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Response', + ["Pool-Id"] = poolId, + ["Status"] = "Success" + }) +end + +return mod diff --git a/ao-contracts/curve-bonded-token/src/handlers/mint.lua b/ao-contracts/curve-bonded-token/src/handlers/mint.lua new file mode 100644 index 00000000..a91b546e --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/handlers/mint.lua @@ -0,0 +1,48 @@ +local bint = require('.bint')(256) +local utils = require "src.utils.mod" +local assertions = require "src.utils.assertions" +local mod = {} + +function mod.mint(msg) + assert(msg.From == BondingCurveProcess, 'Only the bonding curve process can mint!') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(bint.__lt(0, bint(msg.Quantity)), 'Quantity must be greater than zero!') + + -- Check if minting would exceed max supply + local newTotalSupply = utils.add(TotalSupply, msg.Quantity) + + if bint.__lt(bint(MaxSupply), bint(newTotalSupply)) then + msg.reply({ + Action = 'Mint-Error', + Error = 'Minting would exceed max supply!' + }) + + return + end + + -- Calculate required reserve amount + local recipient = msg.Tags.Recipient or msg.From + + assertions.isAddress("Recipient", recipient) + + -- Update balances + if not Balances[recipient] then Balances[recipient] = "0" end + + Balances[recipient] = utils.add(Balances[recipient], msg.Quantity) + TotalSupply = utils.add(TotalSupply, msg.Quantity) + + if msg.reply then + msg.reply({ + Action = 'Mint-Response', + Data = "Successfully minted " .. msg.Quantity + }) + else + ao.send({ + Action = 'Mint-Response', + Target = msg.From, + Data = "Successfully minted " .. msg.Quantity + }) + end +end + +return mod diff --git a/ao-contracts/curve-bonded-token/src/handlers/token.lua b/ao-contracts/curve-bonded-token/src/handlers/token.lua new file mode 100644 index 00000000..f219df33 --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/handlers/token.lua @@ -0,0 +1,89 @@ +local utils = require "src.utils.mod" + +local mod = {} + +--- @type Denomination +Denomination = Denomination or 12 +--- @type Balances +Balances = Balances or { [ao.id] = utils.toBalanceValue(0) } +--- @type TotalSupply +TotalSupply = TotalSupply or utils.toBalanceValue(0) +--- @type Name +Name = Name or "Points Coin" +--- @type Ticker +Ticker = Ticker or "PNTS" +--- @type Logo +Logo = Logo or "SBCCXwwecBlDqRLUjb8dYABExTJXLieawf7m2aBJ-KY" +--- @type MaxSupply +MaxSupply = MaxSupply or nil; +--- @type BondingCurveProcess +BondingCurveProcess = BondingCurveProcess or nil; + +-- Get token info +---@type HandlerFunction +function mod.info(msg) + if msg.reply then + msg.reply({ + Action = 'Info-Response', + Name = Name, + Ticker = Ticker, + Logo = Logo, + Denomination = tostring(Denomination), + MaxSupply = MaxSupply, + TotalSupply = TotalSupply, + BondingCurveProcess = BondingCurveProcess, + }) + else + ao.send({ + Action = 'Info-Response', + Target = msg.From, + Name = Name, + Ticker = Ticker, + Logo = Logo, + Denomination = tostring(Denomination) + }) + end +end + +-- Get token total supply +---@type HandlerFunction +function mod.totalSupply(msg) + assert(msg.From ~= ao.id, 'Cannot call Total-Supply from the same process!') + if msg.reply then + msg.reply({ + Action = 'Total-Supply-Response', + Data = TotalSupply, + Ticker = Ticker + }) + else + Send({ + Target = msg.From, + Action = 'Total-Supply-Response', + Data = TotalSupply, + Ticker = Ticker + }) + end +end + +-- Get token max supply +---@type HandlerFunction +function mod.maxSupply(msg) + assert(msg.From ~= ao.id, 'Cannot call Max-Supply from the same process!') + + if msg.reply then + msg.reply({ + Action = 'Max-Supply-Response', + Data = MaxSupply, + Ticker = Ticker + }) + else + ao.send({ + Target = msg.From, + Action = 'Max-Supply-Response', + Data = MaxSupply, + Ticker = Ticker + }) + end +end + +return mod diff --git a/ao-contracts/curve-bonded-token/src/handlers/token_manager.lua b/ao-contracts/curve-bonded-token/src/handlers/token_manager.lua new file mode 100644 index 00000000..26505abd --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/handlers/token_manager.lua @@ -0,0 +1,136 @@ +local aolibs = require "src.libs.aolibs" +local validations = require "src.utils.validations" +local utils = require "src.utils.mod" +local bint = require('.bint')(256) +local json = aolibs.json +local mod = {} + +--- @type RepoToken +RepoToken = RepoToken or nil +--- @type ReserveToken +ReserveToken = ReserveToken or nil +--- @type ReserveBalance +ReserveBalance = ReserveBalance or '0' +--- @type FundingGoal +FundingGoal = FundingGoal or '0' +--- @type AllocationForLP +AllocationForLP = AllocationForLP or '0' +--- @type AllocationForCreator +AllocationForCreator = AllocationForCreator or '0' +--- @type SupplyToSell +SupplyToSell = SupplyToSell or '0' +--- @type MaxSupply +MaxSupply = MaxSupply or '0' +--- @type Initialized +Initialized = Initialized or false +--- @type ReachedFundingGoal +ReachedFundingGoal = ReachedFundingGoal or false +--- @type LiquidityPool +LiquidityPool = LiquidityPool or nil +--- @type Creator +Creator = Creator or nil + +---@type HandlerFunction +function mod.initialize(msg) + assert(Initialized == false, "TokenManager already initialized") + assert(msg.Data ~= nil, "Data is required") + + --- @type CBTMInitPayload + local initPayload = json.decode(msg.Data) + + if ( + validations.isInvalidInput(initPayload, 'object') or + validations.isInvalidInput(initPayload.repoToken, 'object') or + validations.isInvalidInput(initPayload.repoToken.tokenName, 'string') or + validations.isInvalidInput(initPayload.repoToken.tokenTicker, 'string') or + validations.isInvalidInput(initPayload.repoToken.denomination, 'string') or + validations.isInvalidInput(initPayload.repoToken.tokenImage, 'string') or + validations.isInvalidInput(initPayload.repoToken.processId, 'string') or + validations.isInvalidInput(initPayload.reserveToken, 'object') or + validations.isInvalidInput(initPayload.reserveToken.tokenName, 'string') or + validations.isInvalidInput(initPayload.reserveToken.tokenTicker, 'string') or + validations.isInvalidInput(initPayload.reserveToken.denomination, 'string') or + validations.isInvalidInput(initPayload.reserveToken.tokenImage, 'string') or + validations.isInvalidInput(initPayload.reserveToken.processId, 'string') or + validations.isInvalidInput(initPayload.fundingGoal, 'string') or + validations.isInvalidInput(initPayload.allocationForLP, 'string') or + validations.isInvalidInput(initPayload.allocationForCreator, 'string') or + validations.isInvalidInput(initPayload.maxSupply, 'string') + ) then + if msg.reply then + msg.reply({ + Action = 'Initialize-Error', + Error = 'Invalid inputs supplied.' + }) + return + else + ao.send({ + Target = msg.From, + Action = 'Initialize-Error', + Error = 'Invalid inputs supplied.' + }) + end + end + + local lpAllocation = utils.udivide(utils.multiply(initPayload.maxSupply, "20"), "100") + + local supplyToSell = utils.subtract(initPayload.maxSupply, + utils.add(lpAllocation, initPayload.allocationForCreator)) + + if (bint(supplyToSell) <= 0) then + if msg.reply then + msg.reply({ + Action = 'Initialize-Error', + Error = 'Pre-Allocations and Dex Allocations exceeds max supply' + }) + return + else + ao.send({ + Target = msg.From, + Action = 'Initialize-Error', + Error = 'Pre-Allocations and Dex Allocations exceeds max supply' + }) + return + end + end + + RepoToken = initPayload.repoToken + ReserveToken = initPayload.reserveToken + FundingGoal = utils.toBalanceValue(utils.toSubUnits(initPayload.fundingGoal, ReserveToken.denomination)) + AllocationForLP = utils.toBalanceValue(utils.toSubUnits(lpAllocation, RepoToken.denomination)) + AllocationForCreator = utils.toBalanceValue(utils.toSubUnits(initPayload.allocationForCreator, RepoToken + .denomination)) + MaxSupply = utils.toBalanceValue(utils.toSubUnits(initPayload.maxSupply, RepoToken.denomination)) + SupplyToSell = utils.toBalanceValue(utils.toSubUnits(supplyToSell, RepoToken.denomination)) + Creator = msg.From + + Initialized = true + + msg.reply({ + Action = 'Initialize-Response', + Initialized = true, + }) +end + +---@type HandlerFunction +function mod.info(msg) + msg.reply({ + Action = 'Info-Response', + Data = json.encode({ + reserveBalance = ReserveBalance, + initialized = Initialized, + repoToken = RepoToken, + reserveToken = ReserveToken, + fundingGoal = FundingGoal, + allocationForLP = AllocationForLP, + allocationForCreator = AllocationForCreator, + maxSupply = MaxSupply, + supplyToSell = SupplyToSell, + reachedFundingGoal = ReachedFundingGoal, + liquidityPool = LiquidityPool, + creator = Creator + }) + }) +end + +return mod diff --git a/ao-contracts/curve-bonded-token/src/handlers/transfer.lua b/ao-contracts/curve-bonded-token/src/handlers/transfer.lua new file mode 100644 index 00000000..cdea26f4 --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/handlers/transfer.lua @@ -0,0 +1,81 @@ +local bint = require('.bint')(256) +local utils = require "src.utils.mod" + +local mod = {} + + +function mod.transfer(msg) + assert(type(msg.Recipient) == 'string', 'Recipient is required!') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(bint.__lt(0, bint(msg.Quantity)), 'Quantity must be greater than 0') + + if not Balances[msg.From] then Balances[msg.From] = "0" end + if not Balances[msg.Recipient] then Balances[msg.Recipient] = "0" end + + if bint(msg.Quantity) <= bint(Balances[msg.From]) then + Balances[msg.From] = utils.subtract(Balances[msg.From], msg.Quantity) + Balances[msg.Recipient] = utils.add(Balances[msg.Recipient], msg.Quantity) + + --[[ + Only send the notifications to the Sender and Recipient + if the Cast tag is not set on the Transfer message + ]] + -- + if not msg.Cast then + -- Debit-Notice message template, that is sent to the Sender of the transfer + local debitNotice = { + Action = 'Debit-Notice', + Recipient = msg.Recipient, + Quantity = msg.Quantity, + Data = Colors.gray .. + "You transferred " .. + Colors.blue .. msg.Quantity .. Colors.gray .. " to " .. Colors.green .. msg.Recipient .. Colors.reset + } + -- Credit-Notice message template, that is sent to the Recipient of the transfer + local creditNotice = { + Target = msg.Recipient, + Action = 'Credit-Notice', + Sender = msg.From, + Quantity = msg.Quantity, + Data = Colors.gray .. + "You received " .. + Colors.blue .. msg.Quantity .. Colors.gray .. " from " .. Colors.green .. msg.From .. Colors.reset + } + + -- Add forwarded tags to the credit and debit notice messages + for tagName, tagValue in pairs(msg) do + -- Tags beginning with "X-" are forwarded + if string.sub(tagName, 1, 2) == "X-" then + debitNotice[tagName] = tagValue + creditNotice[tagName] = tagValue + end + end + + -- Send Debit-Notice and Credit-Notice + if msg.reply then + msg.reply(debitNotice) + else + debitNotice.Target = msg.From + Send(debitNotice) + end + Send(creditNotice) + end + else + if msg.reply then + msg.reply({ + Action = 'Transfer-Error', + ['Message-Id'] = msg.Id, + Error = 'Insufficient Balance!' + }) + else + Send({ + Target = msg.From, + Action = 'Transfer-Error', + ['Message-Id'] = msg.Id, + Error = 'Insufficient Balance!' + }) + end + end +end + +return mod diff --git a/ao-contracts/curve-bonded-token/src/libs/aolibs.lua b/ao-contracts/curve-bonded-token/src/libs/aolibs.lua new file mode 100644 index 00000000..b93c23d5 --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/libs/aolibs.lua @@ -0,0 +1,20 @@ +-- These libs should exist in ao + +local mod = {} + +-- Define json + +local cjsonstatus, cjson = pcall(require, "cjson") + +if cjsonstatus then + mod.json = cjson +else + local jsonstatus, json = pcall(require, "json") + if not jsonstatus then + error("Library 'json' does not exist") + else + mod.json = json + end +end + +return mod \ No newline at end of file diff --git a/ao-contracts/curve-bonded-token/src/libs/testing/ao-process.lua b/ao-contracts/curve-bonded-token/src/libs/testing/ao-process.lua new file mode 100644 index 00000000..c88a3d07 --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/libs/testing/ao-process.lua @@ -0,0 +1,161 @@ +local ao = require(".ao") + +_G.ao = ao + +local mod = {} + +---Generate a valid Arweave address +---@return string +local function generateAddress() + local id = "" + + -- possible characters in a valid arweave address + local chars = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890_-" + + while string.len(id) < 43 do + -- get random char + local char = math.random(1, string.len(chars)) + + -- select and apply char + id = id .. string.sub(chars, char, char) + end + + return id +end + +function mod.initialize(msg, env) + -- by requiring '.process' here we are able to reload via .updates + local process = require ".process" + + ao.init(env) + + -- relocate custom tags to root message + msg = ao.normalize(msg) + + -- handle process + pcall(function() return (process.handle(msg, ao)) end) +end + +local processId = generateAddress() +local from = generateAddress() +local owner = from + +local env = { + Module = { + Tags = { + { + name = "Memory-Limit", + value = "1-gb" + }, + { + name = "Compute-Limit", + value = "9000000000000" + }, + { + name = "Module-Format", + value = "wasm64-unknown-emscripten-draft_2024_02_15" + }, + { + name = "Data-Protocol", + value = "ao" + }, + { + name = "Type", + value = "Module" + }, + { + name = "Input-Encoding", + value = "JSON-1" + }, + { + name = "Output-Encoding", + value = "JSON-1" + }, + { + name = "Variant", + value = "ao.TN.1" + }, + { + name = "Content-Type", + value = "application/wasm" + } + }, + Owner = "vh-NTHVvlKZqRxc8LyyTNok65yQ55a_PJ1zWLb9G2JI", + Id = "Pq2Zftrqut0hdisH_MC2pDOT6S4eQFoxGsFUzR6r350" + }, + Process = { + Tags = { + ["App-Name"] = "aos", + ["aos-Version"] = "1.11.3", + [" Data-Protocol"] = "ao", + Scheduler = "_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA", + Variant = "ao.TN.1", + Name = "aos-process", + Type = "Process", + SDK = "aoconnect", + Module = "Pq2Zftrqut0hdisH_MC2pDOT6S4eQFoxGsFUzR6r350", + Authority = "fcoN_xJeisVsPXA-trzVAuIiqO3ydLQxM-L4XbrQKzY" + }, + TagArray = { + { + name = "App-Name", + value = "aos" + }, + { + name = "Name", + value = "aos-process" + }, + { + name = "Authority", + value = "fcoN_xJeisVsPXA-trzVAuIiqO3ydLQxM-L4XbrQKzY" + }, + { + name = "aos-Version", + value = "1.11.3" + }, + { + name = "Data-Protocol", + value = "ao" + }, + { + name = "Variant", + value = "ao.TN.1" + }, + { + name = "Type", + value = "Process" + }, + { + name = "Module", + value = "Pq2Zftrqut0hdisH_MC2pDOT6S4eQFoxGsFUzR6r350" + }, + { + name = "Scheduler", + value = "_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA" + }, + { + name = "SDK", + value = "aoconnect" + } + }, + Owner = owner, + Id = processId + } +} + +mod.initialize( + { + Id = generateAddress(), + Tags = env.Process.Tags, + From = from, + Owner = owner, + Target = processId, + Data = "", + ["Block-Height"] = 1469769, + Module = env.Process.Tags.Module + }, + env +) + +return mod diff --git a/ao-contracts/curve-bonded-token/src/libs/testing/bit/numberlua.lua b/ao-contracts/curve-bonded-token/src/libs/testing/bit/numberlua.lua new file mode 100644 index 00000000..256c5f0a --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/libs/testing/bit/numberlua.lua @@ -0,0 +1,558 @@ +--[[ + +LUA MODULE + + bit.numberlua - Bitwise operations implemented in pure Lua as numbers, + with Lua 5.2 'bit32' and (LuaJIT) LuaBitOp 'bit' compatibility interfaces. + +SYNOPSIS + + local bit = require 'bit.numberlua' + print(bit.band(0xff00ff00, 0x00ff00ff)) --> 0xffffffff + + -- Interface providing strong Lua 5.2 'bit32' compatibility + local bit32 = require 'bit.numberlua'.bit32 + assert(bit32.band(-1) == 0xffffffff) + + -- Interface providing strong (LuaJIT) LuaBitOp 'bit' compatibility + local bit = require 'bit.numberlua'.bit + assert(bit.tobit(0xffffffff) == -1) + +DESCRIPTION + + This library implements bitwise operations entirely in Lua. + This module is typically intended if for some reasons you don't want + to or cannot install a popular C based bit library like BitOp 'bit' [1] + (which comes pre-installed with LuaJIT) or 'bit32' (which comes + pre-installed with Lua 5.2) but want a similar interface. + + This modules represents bit arrays as non-negative Lua numbers. [1] + It can represent 32-bit bit arrays when Lua is compiled + with lua_Number as double-precision IEEE 754 floating point. + + The module is nearly the most efficient it can be but may be a few times + slower than the C based bit libraries and is orders or magnitude + slower than LuaJIT bit operations, which compile to native code. Therefore, + this library is inferior in performane to the other modules. + + The `xor` function in this module is based partly on Roberto Ierusalimschy's + post in http://lua-users.org/lists/lua-l/2002-09/msg00134.html . + + The included BIT.bit32 and BIT.bit sublibraries aims to provide 100% + compatibility with the Lua 5.2 "bit32" and (LuaJIT) LuaBitOp "bit" library. + This compatbility is at the cost of some efficiency since inputted + numbers are normalized and more general forms (e.g. multi-argument + bitwise operators) are supported. + +STATUS + + WARNING: Not all corner cases have been tested and documented. + Some attempt was made to make these similar to the Lua 5.2 [2] + and LuaJit BitOp [3] libraries, but this is not fully tested and there + are currently some differences. Addressing these differences may + be improved in the future but it is not yet fully determined how to + resolve these differences. + + The BIT.bit32 library passes the Lua 5.2 test suite (bitwise.lua) + http://www.lua.org/tests/5.2/ . The BIT.bit library passes the LuaBitOp + test suite (bittest.lua). However, these have not been tested on + platforms with Lua compiled with 32-bit integer numbers. + +API + + BIT.tobit(x) --> z + + Similar to function in BitOp. + + BIT.tohex(x, n) + + Similar to function in BitOp. + + BIT.band(x, y) --> z + + Similar to function in Lua 5.2 and BitOp but requires two arguments. + + BIT.bor(x, y) --> z + + Similar to function in Lua 5.2 and BitOp but requires two arguments. + + BIT.bxor(x, y) --> z + + Similar to function in Lua 5.2 and BitOp but requires two arguments. + + BIT.bnot(x) --> z + + Similar to function in Lua 5.2 and BitOp. + + BIT.lshift(x, disp) --> z + + Similar to function in Lua 5.2 (warning: BitOp uses unsigned lower 5 bits of shift), + + BIT.rshift(x, disp) --> z + + Similar to function in Lua 5.2 (warning: BitOp uses unsigned lower 5 bits of shift), + + BIT.extract(x, field [, width]) --> z + + Similar to function in Lua 5.2. + + BIT.replace(x, v, field, width) --> z + + Similar to function in Lua 5.2. + + BIT.bswap(x) --> z + + Similar to function in Lua 5.2. + + BIT.rrotate(x, disp) --> z + BIT.ror(x, disp) --> z + + Similar to function in Lua 5.2 and BitOp. + + BIT.lrotate(x, disp) --> z + BIT.rol(x, disp) --> z + + Similar to function in Lua 5.2 and BitOp. + + BIT.arshift + + Similar to function in Lua 5.2 and BitOp. + + BIT.btest + + Similar to function in Lua 5.2 with requires two arguments. + + BIT.bit32 + + This table contains functions that aim to provide 100% compatibility + with the Lua 5.2 "bit32" library. + + bit32.arshift (x, disp) --> z + bit32.band (...) --> z + bit32.bnot (x) --> z + bit32.bor (...) --> z + bit32.btest (...) --> true | false + bit32.bxor (...) --> z + bit32.extract (x, field [, width]) --> z + bit32.replace (x, v, field [, width]) --> z + bit32.lrotate (x, disp) --> z + bit32.lshift (x, disp) --> z + bit32.rrotate (x, disp) --> z + bit32.rshift (x, disp) --> z + + BIT.bit + + This table contains functions that aim to provide 100% compatibility + with the LuaBitOp "bit" library (from LuaJIT). + + bit.tobit(x) --> y + bit.tohex(x [,n]) --> y + bit.bnot(x) --> y + bit.bor(x1 [,x2...]) --> y + bit.band(x1 [,x2...]) --> y + bit.bxor(x1 [,x2...]) --> y + bit.lshift(x, n) --> y + bit.rshift(x, n) --> y + bit.arshift(x, n) --> y + bit.rol(x, n) --> y + bit.ror(x, n) --> y + bit.bswap(x) --> y + +DEPENDENCIES + + None (other than Lua 5.1 or 5.2). + +DOWNLOAD/INSTALLATION + + If using LuaRocks: + luarocks install lua-bit-numberlua + + Otherwise, download . + Alternately, if using git: + git clone git://github.com/davidm/lua-bit-numberlua.git + cd lua-bit-numberlua + Optionally unpack: + ./util.mk + or unpack and install in LuaRocks: + ./util.mk install + +REFERENCES + + [1] http://lua-users.org/wiki/FloatingPoint + [2] http://www.lua.org/manual/5.2/ + [3] http://bitop.luajit.org/ + +LICENSE + + (c) 2008-2011 David Manura. Licensed under the same terms as Lua (MIT). + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + (end license) + +--]] + +local M = { _TYPE = 'module', _NAME = 'bit.numberlua', _VERSION = '0.3.1.20120131' } + +local floor = math.floor + +local MOD = 2 ^ 32 +local MODM = MOD - 1 + +local function memoize(f) + local mt = {} + local t = setmetatable({}, mt) + function mt:__index(k) + local v = f(k); t[k] = v + return v + end + + return t +end + +local function make_bitop_uncached(t, m) + local function bitop(a, b) + local res, p = 0, 1 + while a ~= 0 and b ~= 0 do + local am, bm = a % m, b % m + res = res + t[am][bm] * p + a = (a - am) / m + b = (b - bm) / m + p = p * m + end + res = res + (a + b) * p + return res + end + return bitop +end + +local function make_bitop(t) + local op1 = make_bitop_uncached(t, 2 ^ 1) + local op2 = memoize(function(a) + return memoize(function(b) + return op1(a, b) + end) + end) + return make_bitop_uncached(op2, 2 ^ (t.n or 1)) +end + +-- ok? probably not if running on a 32-bit int Lua number type platform +function M.tobit(x) + return x % 2 ^ 32 +end + +M.bxor = make_bitop { [0] = { [0] = 0, [1] = 1 }, [1] = { [0] = 1, [1] = 0 }, n = 4 } +local bxor = M.bxor + +function M.bnot(a) return MODM - a end + +local bnot = M.bnot + +function M.band(a, b) return ((a + b) - bxor(a, b)) / 2 end + +local band = M.band + +function M.bor(a, b) return MODM - band(MODM - a, MODM - b) end + +local bor = M.bor + +local lshift, rshift -- forward declare + +function M.rshift(a, disp) -- Lua5.2 insipred + if disp < 0 then return lshift(a, -disp) end + return floor(a % 2 ^ 32 / 2 ^ disp) +end + +rshift = M.rshift + +function M.lshift(a, disp) -- Lua5.2 inspired + if disp < 0 then return rshift(a, -disp) end + return (a * 2 ^ disp) % 2 ^ 32 +end + +lshift = M.lshift + +function M.tohex(x, n) -- BitOp style + n = n or 8 + local up + if n <= 0 then + if n == 0 then return '' end + up = true + n = -n + end + x = band(x, 16 ^ n - 1) + return ('%0' .. n .. (up and 'X' or 'x')):format(x) +end + +local tohex = M.tohex + +function M.extract(n, field, width) -- Lua5.2 inspired + width = width or 1 + return band(rshift(n, field), 2 ^ width - 1) +end + +local extract = M.extract + +function M.replace(n, v, field, width) -- Lua5.2 inspired + width = width or 1 + local mask1 = 2 ^ width - 1 + v = band(v, mask1) -- required by spec? + local mask = bnot(lshift(mask1, field)) + return band(n, mask) + lshift(v, field) +end + +local replace = M.replace + +function M.bswap(x) -- BitOp style + local a = band(x, 0xff); x = rshift(x, 8) + local b = band(x, 0xff); x = rshift(x, 8) + local c = band(x, 0xff); x = rshift(x, 8) + local d = band(x, 0xff) + return lshift(lshift(lshift(a, 8) + b, 8) + c, 8) + d +end + +local bswap = M.bswap + +function M.rrotate(x, disp) -- Lua5.2 inspired + disp = disp % 32 + local low = band(x, 2 ^ disp - 1) + return rshift(x, disp) + lshift(low, 32 - disp) +end + +local rrotate = M.rrotate + +function M.lrotate(x, disp) -- Lua5.2 inspired + return rrotate(x, -disp) +end + +local lrotate = M.lrotate + +M.rol = M.lrotate -- LuaOp inspired +M.ror = M.rrotate -- LuaOp insipred + + +function M.arshift(x, disp) -- Lua5.2 inspired + local z = rshift(x, disp) + if x >= 0x80000000 then z = z + lshift(2 ^ disp - 1, 32 - disp) end + return z +end + +local arshift = M.arshift + +function M.btest(x, y) -- Lua5.2 inspired + return band(x, y) ~= 0 +end + +-- +-- Start Lua 5.2 "bit32" compat section. +-- + +M.bit32 = {} -- Lua 5.2 'bit32' compatibility + + +local function bit32_bnot(x) + return (-1 - x) % MOD +end +M.bit32.bnot = bit32_bnot + +local function bit32_bxor(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = bxor(a, b) + if c then + z = bit32_bxor(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return 0 + end +end +M.bit32.bxor = bit32_bxor + +local function bit32_band(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = ((a + b) - bxor(a, b)) / 2 + if c then + z = bit32_band(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return MODM + end +end +M.bit32.band = bit32_band + +local function bit32_bor(a, b, c, ...) + local z + if b then + a = a % MOD + b = b % MOD + z = MODM - band(MODM - a, MODM - b) + if c then + z = bit32_bor(z, c, ...) + end + return z + elseif a then + return a % MOD + else + return 0 + end +end +M.bit32.bor = bit32_bor + +function M.bit32.btest(...) + return bit32_band(...) ~= 0 +end + +function M.bit32.lrotate(x, disp) + return lrotate(x % MOD, disp) +end + +function M.bit32.rrotate(x, disp) + return rrotate(x % MOD, disp) +end + +function M.bit32.lshift(x, disp) + if disp > 31 or disp < -31 then return 0 end + return lshift(x % MOD, disp) +end + +function M.bit32.rshift(x, disp) + if disp > 31 or disp < -31 then return 0 end + return rshift(x % MOD, disp) +end + +function M.bit32.arshift(x, disp) + x = x % MOD + if disp >= 0 then + if disp > 31 then + return (x >= 0x80000000) and MODM or 0 + else + local z = rshift(x, disp) + if x >= 0x80000000 then z = z + lshift(2 ^ disp - 1, 32 - disp) end + return z + end + else + return lshift(x, -disp) + end +end + +function M.bit32.extract(x, field, ...) + local width = ... or 1 + if field < 0 or field > 31 or width < 0 or field + width > 32 then error 'out of range' end + x = x % MOD + return extract(x, field, ...) +end + +function M.bit32.replace(x, v, field, ...) + local width = ... or 1 + if field < 0 or field > 31 or width < 0 or field + width > 32 then error 'out of range' end + x = x % MOD + v = v % MOD + return replace(x, v, field, ...) +end + +-- +-- Start LuaBitOp "bit" compat section. +-- + +M.bit = {} -- LuaBitOp "bit" compatibility + +function M.bit.tobit(x) + x = x % MOD + if x >= 0x80000000 then x = x - MOD end + return x +end + +local bit_tobit = M.bit.tobit + +function M.bit.tohex(x, ...) + return tohex(x % MOD, ...) +end + +function M.bit.bnot(x) + return bit_tobit(bnot(x % MOD)) +end + +local function bit_bor(a, b, c, ...) + if c then + return bit_bor(bit_bor(a, b), c, ...) + elseif b then + return bit_tobit(bor(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.bor = bit_bor + +local function bit_band(a, b, c, ...) + if c then + return bit_band(bit_band(a, b), c, ...) + elseif b then + return bit_tobit(band(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.band = bit_band + +local function bit_bxor(a, b, c, ...) + if c then + return bit_bxor(bit_bxor(a, b), c, ...) + elseif b then + return bit_tobit(bxor(a % MOD, b % MOD)) + else + return bit_tobit(a) + end +end +M.bit.bxor = bit_bxor + +function M.bit.lshift(x, n) + return bit_tobit(lshift(x % MOD, n % 32)) +end + +function M.bit.rshift(x, n) + return bit_tobit(rshift(x % MOD, n % 32)) +end + +function M.bit.arshift(x, n) + return bit_tobit(arshift(x % MOD, n % 32)) +end + +function M.bit.rol(x, n) + return bit_tobit(lrotate(x % MOD, n % 32)) +end + +function M.bit.ror(x, n) + return bit_tobit(rrotate(x % MOD, n % 32)) +end + +function M.bit.bswap(x) + return bit_tobit(bswap(x % MOD)) +end + +return M diff --git a/ao-contracts/curve-bonded-token/src/libs/testing/crypto.lua b/ao-contracts/curve-bonded-token/src/libs/testing/crypto.lua new file mode 100644 index 00000000..1c333030 --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/libs/testing/crypto.lua @@ -0,0 +1,3 @@ +local crypto = require('.crypto.init') + +return crypto diff --git a/ao-contracts/curve-bonded-token/src/libs/testing/json.lua b/ao-contracts/curve-bonded-token/src/libs/testing/json.lua new file mode 100644 index 00000000..7a57d8af --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/libs/testing/json.lua @@ -0,0 +1,383 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + ["\\"] = "\\", + ["\""] = "\"", + ["\b"] = "b", + ["\f"] = "f", + ["\n"] = "n", + ["\r"] = "r", + ["\t"] = "t", +} + +local escape_char_map_inv = { ["/"] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + ["nil"] = encode_nil, + ["table"] = encode_table, + ["string"] = encode_string, + ["number"] = encode_number, + ["boolean"] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return (encode(val)) +end + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[select(i, ...)] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + ["true"] = true, + ["false"] = false, + ["null"] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error(string.format("%s at line %d col %d", msg, line_count, col_count)) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error(string.format("invalid unicode codepoint '%x'", n)) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber(s:sub(1, 4), 16) + local n2 = tonumber(s:sub(7, 10), 16) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + ['"'] = parse_string, + ["0"] = parse_number, + ["1"] = parse_number, + ["2"] = parse_number, + ["3"] = parse_number, + ["4"] = parse_number, + ["5"] = parse_number, + ["6"] = parse_number, + ["7"] = parse_number, + ["8"] = parse_number, + ["9"] = parse_number, + ["-"] = parse_number, + ["t"] = parse_literal, + ["f"] = parse_literal, + ["n"] = parse_literal, + ["["] = parse_array, + ["{"] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + +return json diff --git a/ao-contracts/curve-bonded-token/src/types.lua b/ao-contracts/curve-bonded-token/src/types.lua new file mode 100644 index 00000000..4813ddf2 --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/types.lua @@ -0,0 +1,42 @@ +---@meta _ + +---@alias Balances table +---@alias TotalSupply string +---@alias Name string +---@alias Ticker string +---@alias Denomination number +---@alias Logo string +---@alias MaxSupply string +---@alias BondingCurveProcess string + + +-- Curve Bonded Token Manager States + +---@alias RepoToken TokenDetails +---@alias ReserveToken TokenDetails +---@alias ReserveBalance string +---@alias FundingGoal string +---@alias AllocationForLP string +---@alias AllocationForCreator string +---@alias SupplyToSell string +---@alias Initialized boolean +---@alias ReachedFundingGoal boolean +---@alias LiquidityPool string | nil +---@alias Creator string + +---@class TokenDetails +---@field tokenName string +---@field tokenTicker string +---@field denomination number +---@field tokenImage string +---@field processId string + +---@class CBTMInitPayload +---@field repoToken TokenDetails +---@field reserveToken TokenDetails +---@field fundingGoal string +---@field allocationForLP string +---@field allocationForCreator string +---@field maxSupply string + + diff --git a/ao-contracts/curve-bonded-token/src/utils/assertions.lua b/ao-contracts/curve-bonded-token/src/utils/assertions.lua new file mode 100644 index 00000000..b6947649 --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/utils/assertions.lua @@ -0,0 +1,55 @@ +local Type = require "arweave.types.type" + +local mod = {} + +---Assert value is an Arweave address +---@param name string +---@param value string +mod.isAddress = function(name, value) + Type + :string("Invalid type for `" .. name .. "`. Expected a string for Arweave address.") + :length(43, nil, "Incorrect length for Arweave address `" .. name .. "`. Must be exactly 43 characters long.") + :match("[a-zA-Z0-9-_]+", + "Invalid characters in Arweave address `" .. + name .. "`. Only alphanumeric characters, dashes, and underscores are allowed.") + :assert(value) +end + +---Assert value is an UUID +---@param name string +---@param value string +mod.isUuid = function(name, value) + Type + :string("Invalid type for `" .. name .. "`. Expected a string for UUID.") + :match("^[0-9a-fA-F]%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$", + "Invalid UUID format for `" .. name .. "`. A valid UUID should follow the 8-4-4-4-12 hexadecimal format.") + :assert(value) +end + +mod.Array = Type:array("Invalid type (must be array)") + +-- string assertion +mod.String = Type:string("Invalid type (must be a string)") + +-- Assert not empty string +---@param value any Value to assert for +---@param message string? Optional message to throw +---@param len number Required length +---@param match_type? "less"|"greater" String length should be "less" than or "greater" than the defined length. Leave empty for exact match. +---@param len_message string? Optional assertion error message for length +mod.assertNotEmptyString = function(value, message, len, match_type, len_message) + Type:string(message):length(len, match_type, len_message):assert(value) +end + +-- number assertion +mod.Integer = Type:number():integer("Invalid type (must be a integer)") +-- number assertion +mod.Number = Type:number("Invalid type (must be a number)") + +-- repo name assertion +mod.RepoName = Type + :string("Invalid type for Repository name (must be a string)") + :match("^[a-zA-Z0-9._-]+$", + "The repository name can only contain ASCII letters, digits, and the characters ., -, and _") + +return mod diff --git a/ao-contracts/curve-bonded-token/src/utils/helpers.lua b/ao-contracts/curve-bonded-token/src/utils/helpers.lua new file mode 100644 index 00000000..6a671fdd --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/utils/helpers.lua @@ -0,0 +1,94 @@ +local bint = require('.bint')(256) +local utils = require "src.utils.mod" + +local mod = {} + +function mod.find(predicate, arr) + for _, value in ipairs(arr) do + if predicate(value) then + return value + end + end + return nil +end + +function mod.filter(predicate, arr) + local result = {} + for _, value in ipairs(arr) do + if predicate(value) then + table.insert(result, value) + end + end + return result +end + +function mod.reduce(reducer, initialValue, arr) + local result = initialValue + for i, value in ipairs(arr) do + result = reducer(result, value, i, arr) + end + return result +end + + +function mod.map(mapper, arr) + local result = {} + for i, value in ipairs(arr) do + result[i] = mapper(value, i, arr) + end + return result +end + +function mod.reverse(arr) + local result = {} + for i = #arr, 1, -1 do + table.insert(result, arr[i]) + end + return result +end + +function mod.compose(...) + local funcs = { ... } + return function(x) + for i = #funcs, 1, -1 do + x = funcs[i](x) + end + return x + end +end + +function mod.keys(xs) + local ks = {} + for k, _ in pairs(xs) do + table.insert(ks, k) + end + return ks +end + +function mod.values(xs) + local vs = {} + for _, v in pairs(xs) do + table.insert(vs, v) + end + return vs +end + +function mod.includes(value, arr) + for _, v in ipairs(arr) do + if v == value then + return true + end + end + return false +end + +function mod.computeTotalSupply() + local r = mod.reduce( + function(acc, val) return acc + val end, + bint.zero(), + mod.values(Balances)) + + return r +end + +return mod diff --git a/ao-contracts/curve-bonded-token/src/utils/mod.lua b/ao-contracts/curve-bonded-token/src/utils/mod.lua new file mode 100644 index 00000000..cc33553c --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/utils/mod.lua @@ -0,0 +1,30 @@ +local bint = require('.bint')(256) + +local utils = { + add = function(a, b) + return tostring(bint(a) + bint(b)) + end, + subtract = function(a, b) + return tostring(bint(a) - bint(b)) + end, + multiply = function(a, b) + return tostring(bint(a) * bint(b)) + end, + divide = function(a, b) + return tostring(bint(a) / bint(b)) + end, + udivide = function(a, b) + return tostring(bint.udiv(bint(a), bint(b))) + end, + toBalanceValue = function(a) + return tostring(bint(a)) + end, + toNumber = function(a) + return tonumber(a) + end, + toSubUnits = function(val, denom) + return bint(val) * bint.ipow(bint(10), bint(denom)) + end +} + +return utils diff --git a/ao-contracts/curve-bonded-token/src/utils/patterns.lua b/ao-contracts/curve-bonded-token/src/utils/patterns.lua new file mode 100644 index 00000000..723b10ae --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/utils/patterns.lua @@ -0,0 +1,83 @@ +local aolibs = require "src.libs.aolibs" +local json = aolibs.json + +local mod = {} + +function mod.continue(fn) + return function(msg) + local patternResult = fn(msg) + if not patternResult or patternResult == 0 or patternResult == "skip" then + return 0 + end + return 1 + end + end + + + + + + + function mod.hasMatchingTagOf(name, values) + return function(msg) + for _, value in ipairs(values) do + local patternResult = Handlers.utils.hasMatchingTag(name, value)(msg) + + + if patternResult ~= 0 and patternResult ~= false and patternResult ~= "skip" then + return 1 + end + end + + return 0 + end + end + + + + + + function mod._and(patterns) + return function(msg) + for _, pattern in ipairs(patterns) do + local patternResult = pattern(msg) + + if not patternResult or patternResult == 0 or patternResult == "skip" then + return 0 + end + end + + return -1 + end + end + + function mod.catchWrapper(handler, handlerName) + + local nameString = handlerName and handlerName .. " - " or "" + + return function(msg, env) + + local status + local result + + status, result = pcall(handler, msg, env) + + + if not status then + local traceback = debug.traceback() + + print("!!! Error: " .. nameString .. json.encode(traceback)) + local err = string.gsub(result, "%[[%w_.\" ]*%]:%d*: ", "") + + + + RefundError = err + + return nil + end + + return result + end + end + + return mod \ No newline at end of file diff --git a/ao-contracts/curve-bonded-token/src/utils/validations.lua b/ao-contracts/curve-bonded-token/src/utils/validations.lua new file mode 100644 index 00000000..34e8257c --- /dev/null +++ b/ao-contracts/curve-bonded-token/src/utils/validations.lua @@ -0,0 +1,89 @@ +local mod = {} + +local regexPatterns = { + uuid = "^[0-9a-fA-F]%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$", + address = "^[a-zA-Z0-9-_]+$", + email = "^%w+@[%w%.]+%.%w+$", + url = "^%w+://[%w%.]+%.%w+", + username = "^[a-zA-Z0-9]+$" +} + +-- Helper function for pattern matching +local function matches(input, pattern) + return string.match(input, pattern) ~= nil +end + +local function endsWith(str, ending) + return ending == "" or str:sub(- #ending) == ending +end + +-- Type checking functions +function mod.isUuid(input) + return type(input) == 'string' and matches(input, regexPatterns.uuid) +end + +function mod.isArweaveAddress(input) + return type(input) == 'string' and #input == 43 and matches(input, regexPatterns.address) +end + +function mod.isObject(input) + return type(input) == 'table' and not (getmetatable(input) or {}).__isarray +end + +function mod.isArray(input) + return type(input) == 'table' and (getmetatable(input) or {}).__isarray +end + +function mod.isEmail(input, skipEmptyStringCheck) + if skipEmptyStringCheck and input == '' then return true end + return type(input) == 'string' and matches(input, regexPatterns.email) +end + +function mod.isUsername(input) + return type(input) == 'string' and #input >= 4 and #input <= 39 and not endsWith(input, "-") and + matches(input, regexPatterns.username) +end + +function mod.isURL(input, skipEmptyStringCheck) + if skipEmptyStringCheck and input == '' then return true end + return type(input) == 'string' and matches(input, regexPatterns.url) +end + +-- Main type checking function +local function isType(input, expectedType, skipEmptyStringCheck) + if expectedType == 'object' then + return mod.isObject(input) + elseif expectedType == 'array' then + return mod.isArray(input) + elseif expectedType == 'uuid' then + return mod.isUuid(input) + elseif expectedType == 'arweave-address' then + return mod.isArweaveAddress(input) + elseif expectedType == 'url' then + return mod.isURL(input, skipEmptyStringCheck) + elseif expectedType == 'email' then + return mod.isEmail(input, skipEmptyStringCheck) + elseif expectedType == 'username' then + return mod.isUsername(input) + else + return type(input) == expectedType + end +end + +-- Validation function +function mod.isInvalidInput(input, expectedTypes, skipEmptyStringCheck) + skipEmptyStringCheck = skipEmptyStringCheck or false + if input == nil or (not skipEmptyStringCheck and input == '') then + return true + end + + if type(expectedTypes) ~= 'table' then expectedTypes = { expectedTypes } end + for _, expectedType in ipairs(expectedTypes) do + if isType(input, expectedType, skipEmptyStringCheck) then + return false + end + end + return true +end + +return mod diff --git a/ao-contracts/curve-bonded-token/tests/contract_spec.lua b/ao-contracts/curve-bonded-token/tests/contract_spec.lua new file mode 100644 index 00000000..8cac59ef --- /dev/null +++ b/ao-contracts/curve-bonded-token/tests/contract_spec.lua @@ -0,0 +1,173 @@ +local testing = require "arweave.testing" +local json = require "json" +local utils = require "src.utils.mod" +local testUtils = require "tests.utils" + +require "ao-process" +require "src.contract" + +ao = mock(ao) + +-- Token globals +_G.Name = "Test Token" +_G.Ticker = "TTKN" +_G.Denomination = 12 +_G.Balances = { [ao.id] = utils.toBalanceValue(10000 * 10 ^ Denomination) } +TotalSupply = TotalSupply or utils.toBalanceValue(10000 * 10 ^ Denomination) +_G.Logo = "SBCCXwwecBlDqRLUjb8dYABExTJXLieawf7m2aBJ-KY" + +describe("Token", function() + before_each(function() + -- reset mocks + ao.send:clear() + end) + + test("Info", function() + local msg = { + From = testing.utils.generateAddress() + } + + testUtils.sendMessageToHandler("Info", msg) + + local expectedOutput = { + Target = msg.From, + Name = Name, + Ticker = Ticker, + Denomination = tostring(Denomination), + Logo = Logo, + } + + assert.spy(ao.send).was.called_with(expectedOutput) + end) + + test("Total Supply", function() + local msg = { + From = testing.utils.generateAddress() + } + + testUtils.sendMessageToHandler("Total-Supply", msg) + + local expectedOutput = { + Target = msg.From, + Action = 'Total-Supply', + Data = TotalSupply, + Ticker = Ticker, + } + + assert.spy(ao.send).was.called_with(expectedOutput) + end) + + test("Balance", function() + local msg = { + From = testing.utils.generateAddress(), + Tags = { + Target = ao.id + } + } + + testUtils.sendMessageToHandler("Balance", msg) + + local expectedOutput = { + Target = msg.From, + Balance = Balances[ao.id], + Ticker = Ticker, + Account = msg.From, + Data = Balances[ao.id] + } + + assert.spy(ao.send).was.called_with(expectedOutput) + end) + + test("Balances", function() + local msg = { + From = testing.utils.generateAddress(), + } + + testUtils.sendMessageToHandler("Balances", msg) + + local expectedOutput = { + Target = msg.From, + Data = json.encode(Balances) + } + + assert.spy(ao.send).was.called_with(expectedOutput) + end) + + test("Transfer", function() + local msg = { + From = ao.id, + Recipient = testing.utils.generateAddress(), + Quantity = utils.toBalanceValue(5000 * 10 ^ Denomination) + } + + testUtils.sendMessageToHandler("Transfer", msg) + + local expectedCreditNotice = { + Target = msg.Recipient, + Action = 'Credit-Notice', + Sender = msg.From, + Quantity = msg.Quantity, + Data = "You received " .. msg.Quantity .. " from " .. msg.From + } + + local expectedDebitNotice = { + Target = msg.From, + Action = 'Debit-Notice', + Recipient = msg.Recipient, + Quantity = msg.Quantity, + Data = "You transferred " .. msg.Quantity .. " to " .. msg.Recipient + } + + assert.spy(ao.send).was.called_with(expectedCreditNotice) + assert.spy(ao.send).was.called_with(expectedDebitNotice) + end) + + test("Transfer Fail", function() + local msg = { + From = ao.id, + Recipient = testing.utils.generateAddress(), + Quantity = utils.toBalanceValue(0 ^ Denomination) + } + + assert.has_error(function() testUtils.sendMessageToHandler("Transfer", msg) end, + 'Quantity must be greater than 0') + end) + + test("Mint", function() + local msg = { + From = ao.id, + Quantity = utils.toBalanceValue(5000 * 10 ^ Denomination) + } + + local prevBalance = Balances[msg.From] or "0" + + testUtils.sendMessageToHandler("Mint", msg) + + local expectedOutput = { + Target = msg.From, + Data = "Successfully minted " .. msg.Quantity + } + + assert(Balances[ao.id] == utils.add(prevBalance, msg.Quantity), "Balance mismatch!") + assert.spy(ao.send).was.called_with(expectedOutput) + end) + + test("Burn", function() + local msg = { + From = ao.id, + Quantity = utils.toBalanceValue(5000 * 10 ^ Denomination) + } + + local prevBalance = Balances[msg.From] or "0" + + testUtils.sendMessageToHandler("Burn", msg) + + local expectedOutput = { + Target = msg.From, + Data = "Successfully burned " .. msg.Quantity + } + + assert(Balances[ao.id] == utils.subtract(prevBalance, msg.Quantity), "Balance mismatch!") + assert.spy(ao.send).was.called_with(expectedOutput) + end) +end) diff --git a/ao-contracts/curve-bonded-token/tests/utils.lua b/ao-contracts/curve-bonded-token/tests/utils.lua new file mode 100644 index 00000000..d2279bb7 --- /dev/null +++ b/ao-contracts/curve-bonded-token/tests/utils.lua @@ -0,0 +1,13 @@ +local utils = {} + +function utils.sendMessageToHandler(name, msg) + for _, v in ipairs(Handlers.list) do + if v.name == name then + v.handle(msg) + return + end + end + error("Handle " .. name .. " not found") +end + +return utils diff --git a/ao-contracts/discrete-bonding-curve/README.md b/ao-contracts/discrete-bonding-curve/README.md new file mode 100644 index 00000000..8e13225f --- /dev/null +++ b/ao-contracts/discrete-bonding-curve/README.md @@ -0,0 +1 @@ +coming soon... diff --git a/package.json b/package.json index 873ecd5a..6dc2fe6e 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,20 @@ "@isomorphic-git/lightning-fs": "^4.6.0", "@markdoc/markdoc": "^0.4.0", "@permaweb/aoconnect": "^0.0.52", + "@protocol.land/isomorphic-git": "^1.1.0", + "@ramonak/react-progress-bar": "^5.3.0", + "@types/numeral": "^2.0.5", "@uiw/codemirror-extensions-langs": "^4.21.11", "@uiw/codemirror-theme-github": "^4.21.11", "@uiw/react-codemirror": "^4.21.11", "@uiw/react-md-editor": "^3.23.5", + "@xyflow/react": "^12.0.4", + "ar-gql": "^2.0.2", "ardb": "^1.1.10", + "arfs-js": "^1.4.2", "arweave": "^1.14.4", + "bignumber.js": "^9.1.2", + "canvas-confetti": "^1.9.3", "clsx": "^2.0.0", "date-fns": "^2.30.0", "dexie": "^3.2.4", @@ -38,11 +46,12 @@ "framer-motion": "^10.15.0", "headless-stepper": "^1.9.1", "immer": "^10.0.2", - "isomorphic-git": "^1.24.5", "js-yaml": "^4.1.0", "jszip": "^3.10.1", "lang-exts-map": "^0.4.0", + "lightweight-charts": "^4.2.1", "mime": "^4.0.0", + "numeral": "^2.0.6", "react": "^18.2.0", "react-calendar-heatmap": "^1.9.0", "react-codemirror-merge": "^4.21.13", @@ -73,6 +82,7 @@ "zustand": "^4.4.0" }, "devDependencies": { + "@types/canvas-confetti": "^1.6.4", "@types/clean-git-ref": "^2.0.2", "@types/js-yaml": "^4.0.9", "@types/node": "^20.4.6", diff --git a/public/contracts/curve-bonded-token-manager.lua b/public/contracts/curve-bonded-token-manager.lua new file mode 100644 index 00000000..3e6e64a7 --- /dev/null +++ b/public/contracts/curve-bonded-token-manager.lua @@ -0,0 +1,1035 @@ +-- module: "src.utils.mod" +local function _loaded_mod_src_utils_mod() + local bint = require('.bint')(256) + + local utils = { + add = function(a, b) + return tostring(bint(a) + bint(b)) + end, + subtract = function(a, b) + return tostring(bint(a) - bint(b)) + end, + multiply = function(a, b) + return tostring(bint(a) * bint(b)) + end, + divide = function(a, b) + return tostring(bint(a) / bint(b)) + end, + udivide = function(a, b) + return tostring(bint.udiv(bint(a), bint(b))) + end, + toBalanceValue = function(a) + return tostring(bint(a)) + end, + toNumber = function(a) + return tonumber(a) + end, + toSubUnits = function(val, denom) + return bint(val) * bint.ipow(bint(10), bint(denom)) + end + } + + return utils + +end + +_G.package.loaded["src.utils.mod"] = _loaded_mod_src_utils_mod() + +-- module: "src.libs.aolibs" +local function _loaded_mod_src_libs_aolibs() + -- These libs should exist in ao + + local mod = {} + + -- Define json + + local cjsonstatus, cjson = pcall(require, "cjson") + + if cjsonstatus then + mod.json = cjson + else + local jsonstatus, json = pcall(require, "json") + if not jsonstatus then + error("Library 'json' does not exist") + else + mod.json = json + end + end + + return mod +end + +_G.package.loaded["src.libs.aolibs"] = _loaded_mod_src_libs_aolibs() + +-- module: "src.handlers.bonding_curve" +local function _loaded_mod_src_handlers_bonding_curve() + local utils = require "src.utils.mod" + local aolibs = require "src.libs.aolibs" + local bint = require('.bint')(256) + local json = aolibs.json + + local mod = {} + + --[[ + --- Get price for purchasing tokenQuantity of tokens + --]] + local EXP_N = 2 -- exponent determining the rate of increase + local EXP_N_PLUS1 = EXP_N + 1 + + --- @type table + RefundsMap = {} + + + function GetCurrentSupply() + Send({ Target = RepoToken.processId, Action = "Total-Supply" }) + local currentSupplyResp = Receive( + function(m) + return m.Tags['From-Process'] == RepoToken.processId and + m.Tags['Action'] == 'Total-Supply-Response' + end) + + return currentSupplyResp.Data + end + + function RefundHandler(amount, target, pid) + local refundResp = ao.send({ + Target = pid, + Action = "Transfer", + Recipient = target, + Quantity = tostring(amount), + }).receive() + + if refundResp.Tags['Action'] ~= 'Debit-Notice' then + RefundsMap[target] = tostring(amount) + msg.reply({ + Action = 'Refund-Error', + Error = 'Refund failed' + }) + + return false + end + + return true + end + + function LogActivity(action, data, msg) + ao.send({ Target = ao.id, Action = "Activity-Log", ["User-Action"] = action, Data = data, Message = msg }) + end + + ---@type HandlerFunction + function mod.getBuyPrice(msg) + local tokensToBuy = msg.Tags['Token-Quantity'] + local currentSupply = msg.Tags['Current-Supply'] + assert(type(tokensToBuy) == 'string', 'Token quantity is required!') + + local tokensToBuyInSubUnits = utils.toSubUnits(tokensToBuy, RepoToken.denomination) + assert(bint.__lt(0, tokensToBuyInSubUnits), 'Token quantity must be greater than zero!') + + if (Initialized ~= true or RepoToken == nil or RepoToken.processId == nil) then + msg.reply({ + Action = 'Get-Buy-Price-Error', + Error = 'Bonding curve not initialized' + }) + + return + end + if (currentSupply == nil) then + msg.reply({ + Action = 'Get-Buy-Price-Error', + Error = 'Failed to get current supply of curve bonded token' + }) + + return + end + + -- current supply is returned in sub units + -- local preAllocation = utils.add(AllocationForLP, AllocationForCreator) + local s1 = currentSupply + local s2 = utils.add(currentSupply, tokensToBuyInSubUnits); + + if bint.__lt(bint(SupplyToSell), bint(s2)) then + local diff = utils.subtract(SupplyToSell, currentSupply) + msg.reply({ + Action = 'Get-Buy-Price-Error', + Error = 'Not enough tokens to sell. Remaining: ' .. diff + }) + + return + end + + local S_exp = bint.ipow(bint(SupplyToSell), bint(EXP_N_PLUS1)) + + if bint.__le(S_exp, 0) then + msg.reply({ + Action = 'Get-Buy-Price-Error', + Error = 'Bonding curve error: S_EXP too low ' .. S_exp + }) + + return + end + + -- Cost = G * [ (s2)^(n+1) - (s1)^(n+1) ] / S^(n+1) + local s1_exp = bint.ipow(bint(s1), bint(EXP_N_PLUS1)) + local s2_exp = bint.ipow(bint(s2), bint(EXP_N_PLUS1)) + + local numerator = utils.multiply(FundingGoal, utils.subtract(s2_exp, s1_exp)) + local cost = utils.divide(numerator, S_exp) + + local roundedCost = math.ceil(utils.toNumber(cost)) + + msg.reply({ + Action = 'Get-Buy-Price-Response', + Price = utils.toBalanceValue(roundedCost), + CurrentSupply = currentSupply, + TokensToBuy = tokensToBuy, + Data = utils.toBalanceValue(roundedCost), + Denomination = ReserveToken.denomination, + Ticker = ReserveToken.tokenTicker, + RawPrice = cost + }) + end + + --[[ + --- Get price for selling tokenQuantity of tokens + --- + --]] + ---@type HandlerFunction + function mod.getSellPrice(msg) + local tokensToSell = msg.Tags['Token-Quantity'] + assert(type(tokensToSell) == 'string', 'Token quantity is required!') + + if bint.__le(bint(ReserveBalance), 0) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'No reserve balance to sell!' + }) + + return + end + + local tokensToSellInSubUnits = utils.toSubUnits(tokensToSell, RepoToken.denomination) + assert(bint.__lt(0, tokensToSellInSubUnits), 'Token quantity must be greater than zero!') + + if (Initialized ~= true or RepoToken == nil or RepoToken.processId == nil) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'Bonding curve not initialized' + }) + + return + end + + ao.send({ Target = RepoToken.processId, Action = "Total-Supply" }) + local currentSupplyResp = Receive( + function(m) + return m.Tags['From-Process'] == RepoToken.processId and + m.Data ~= nil + end) + + if (currentSupplyResp == nil or currentSupplyResp.Data == nil) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'Failed to get current supply of curve bonded token' + }) + + return + end + + -- current supply is returned in sub units + local preAllocation = utils.add(AllocationForLP, AllocationForCreator) + local s1 = utils.subtract(currentSupplyResp.Data, preAllocation) + local s2 = utils.subtract(s1, tokensToSellInSubUnits); + + local S_exp = bint.ipow(bint(SupplyToSell), bint(EXP_N_PLUS1)) + + if bint.__le(S_exp, 0) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'Bonding curve error: S_EXP too low ' .. S_exp + }) + + return + end + + -- Cost = G * [ (s2)^(n+1) - (s1)^(n+1) ] / S^(n+1) + local s1_exp = bint.ipow(bint(s1), bint(EXP_N_PLUS1)) + local s2_exp = bint.ipow(bint(s2), bint(EXP_N_PLUS1)) + + local numerator = utils.multiply(FundingGoal, utils.subtract(s2_exp, s1_exp)) + local cost = utils.divide(numerator, S_exp) + + if bint.__lt(bint(cost), bint(ReserveBalance)) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'Bonding curve error: Insufficient reserve balance to sell: ' .. cost + }) + + return + end + + msg.reply({ + Action = 'Get-Sell-Price-Response', + Price = cost, + Data = cost, + Denomination = ReserveToken.denomination, + Ticker = ReserveToken.tokenTicker + }) + end + + ---@type HandlerFunction + function mod.buyTokens(msg, env) + LogActivity(msg.Tags['X-Action'], json.encode(msg.Tags), "Buy-Tokens Called") + local reservePID = msg.From + + local sender = msg.Tags.Sender + + local quantityReservesSent = bint(msg.Tags.Quantity) + + -- local slippageTolerance = tonumber(msg.Tags["X-Slippage-Tolerance"]) or 0 + + assert(ReachedFundingGoal == false, 'Funding goal has been reached!') + + local tokensToBuy = msg.Tags['X-Token-Quantity'] + assert(type(tokensToBuy) == 'string', 'Token quantity is required!') + + local tokensToBuyInSubUnits = utils.toSubUnits(tokensToBuy, RepoToken.denomination) + assert(bint.__lt(0, tokensToBuyInSubUnits), 'Token quantity must be greater than zero!') + + if (Initialized ~= true or RepoToken == nil or RepoToken.processId == nil) then + msg.reply({ + Action = 'Buy-Tokens-Error', + Error = 'Bonding curve not initialized' + }) + + return + end + + -- double call issue + local currentSupplyResp = ao.send({ Target = RepoToken.processId, Action = "Total-Supply" }).receive().Data + -- local currentSupplyResp = msg.Tags['X-Current-Supply'] + if (currentSupplyResp == nil) then + msg.reply({ + Action = 'Buy-Tokens-Error', + Error = 'Failed to get current supply of curve bonded token' + }) + + return + end + + currentSupplyResp = tostring(currentSupplyResp) + + -- current supply is returned in sub units + -- local preAllocation = utils.add(AllocationForLP, AllocationForCreator) + local s1 = currentSupplyResp + local s2 = utils.add(currentSupplyResp, tokensToBuyInSubUnits); + -- Calculate remaining tokens + local remainingTokens = utils.subtract(SupplyToSell, currentSupplyResp) + + -- Check if there are enough tokens to sell + if bint.__lt(bint(remainingTokens), bint(tokensToBuyInSubUnits)) then + msg.reply({ + Action = 'Buy-Tokens-Error', + Remaining = tostring(remainingTokens), + TokensToBuy = tostring(tokensToBuyInSubUnits), + Error = 'Not enough tokens to sell.' + }) + return + end + + local S_exp = bint.ipow(bint(SupplyToSell), bint(EXP_N_PLUS1)) + + if bint.__le(S_exp, 0) then + msg.reply({ + Action = 'Buy-Tokens-Error', + Error = 'Bonding curve error: S_EXP too low ' .. S_exp + }) + + return + end + + -- Cost = G * [ (s2)^(n+1) - (s1)^(n+1) ] / S^(n+1) + local s1_exp = bint.ipow(bint(s1), bint(EXP_N_PLUS1)) + local s2_exp = bint.ipow(bint(s2), bint(EXP_N_PLUS1)) + + local numerator = utils.multiply(FundingGoal, utils.subtract(s2_exp, s1_exp)) + local cost = utils.divide((numerator), S_exp) + LogActivity(msg.Tags['X-Action'], json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Calculated cost of buying tokens for Reserves sent") + if bint.__lt(bint(quantityReservesSent), bint(math.ceil(utils.toNumber(cost)))) then + LogActivity(msg.Tags['X-Action'], + json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Insufficient funds sent to buy") + local refundSuccess = RefundHandler(quantityReservesSent, sender, reservePID) + + if not refundSuccess then + LogActivity(msg.Tags['X-Action'], + json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Refund failed") + return + end + + ao.send({ + Target = sender, + Action = "Refund-Notice", + Quantity = tostring(quantityReservesSent), + }) + + msg.reply({ + Cost = tostring(cost), + AmountSent = tostring(quantityReservesSent), + Action = 'Buy-Tokens-Error', + Error = 'Insufficient funds sent to buy' + }) + + return + end + + local mintResp = ao.send({ Target = RepoToken.processId, Action = "Mint", Tags = { Quantity = utils.toBalanceValue(tokensToBuyInSubUnits), Recipient = sender } }) + .receive() + + if mintResp.Tags['Action'] ~= 'Mint-Response' then + LogActivity(msg.Tags['X-Action'], + json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Failed to mint tokens") + local refundSuccess = RefundHandler(quantityReservesSent, sender, reservePID) + + if not refundSuccess then + LogActivity(msg.Tags['X-Action'], + json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Refund failed after failed mint") + return + end + + ao.send({ + Target = sender, + Action = "Refund-Notice", + Quantity = tostring(quantityReservesSent), + }) + msg.reply({ + Action = 'Buy-Tokens-Error', + Error = 'Failed to mint tokens. Amount will be refunded.' + }) + + return + end + + ReserveBalance = utils.add(ReserveBalance, quantityReservesSent) + if bint(ReserveBalance) >= bint(FundingGoal) then + ReachedFundingGoal = true + end + + LogActivity(msg.Tags['X-Action'], json.encode({ Cost = tostring(cost), AmountSent = tostring(quantityReservesSent) }), + "Successfully bought tokens") + + msg.reply({ + Action = 'Buy-Tokens-Response', + TokensBought = utils.toBalanceValue(tokensToBuyInSubUnits), + Cost = tostring(cost), + Data = mintResp.Data or ('Successfully bought ' .. tokensToBuy .. ' tokens') + }) + end + + ---@type HandlerFunction + function mod.sellTokens(msg) + LogActivity(msg.Tags['Action'], json.encode(msg.Tags), "Sell-Tokens Called") + assert(ReachedFundingGoal == false, 'Funding goal has been reached!') + + local tokensToSell = msg.Tags['Token-Quantity'] + assert(type(tokensToSell) == 'string', 'Token quantity is required!') + + if bint.__le(bint(ReserveBalance), 0) then + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'No reserve balance to sell!' + }) + + return + end + + local tokensToSellInSubUnits = utils.toSubUnits(tokensToSell, RepoToken.denomination) + assert(bint.__lt(0, tokensToSellInSubUnits), 'Token quantity must be greater than zero!') + + if (Initialized ~= true or RepoToken == nil or RepoToken.processId == nil) then + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Bonding curve not initialized' + }) + + return + end + + local currentSupplyResp = ao.send({ Target = RepoToken.processId, Action = "Total-Supply" }).receive() + if (currentSupplyResp == nil or currentSupplyResp.Data == nil) then + msg.reply({ + Action = 'Get-Sell-Price-Error', + Error = 'Failed to get current supply of curve bonded token' + }) + + return + end + + if bint.__le(bint(currentSupplyResp.Data), 0) then + LogActivity(msg.Tags['Action'], + json.encode({ CurrentSupply = currentSupplyResp.Data, TokensToSell = tokensToSell }), + "No tokens to sell. Buy some tokens first.") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'No tokens to sell. Buy some tokens first.' + }) + + return + end + + -- Check if there are enough tokens to sell + if bint.__lt(bint(currentSupplyResp.Data), bint(tokensToSellInSubUnits)) then + LogActivity(msg.Tags['Action'], + json.encode({ CurrentSupply = currentSupplyResp.Data, TokensToSell = tostring(tokensToSellInSubUnits) }), + "Not enough tokens to sell.") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Not enough tokens to sell.' + }) + return + end + + -- current supply is returned in sub units + -- local preAllocation = utils.add(AllocationForLP, AllocationForCreator) + local s1 = currentSupplyResp.Data + local s2 = utils.subtract(currentSupplyResp.Data, tokensToSellInSubUnits); + + local S_exp = bint.ipow(bint(SupplyToSell), bint(EXP_N_PLUS1)) + + if bint.__le(S_exp, 0) then + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Bonding curve error: S_EXP too low ' .. S_exp + }) + + return + end + + -- Cost = G * [ (s2)^(n+1) - (s1)^(n+1) ] / S^(n+1) + local s1_exp = bint.ipow(bint(s1), bint(EXP_N_PLUS1)) + local s2_exp = bint.ipow(bint(s2), bint(EXP_N_PLUS1)) + + local numerator = utils.multiply(FundingGoal, utils.subtract(s1_exp, s2_exp)) + local cost = utils.divide(numerator, S_exp) + + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits) + }), "Calculated cost of selling tokens") + + local balanceResp = ao.send({ Target = ReserveToken.processId, Action = "Balance" }).receive() + if balanceResp == nil or balanceResp.Data == nil then + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits) + }), + "Failed to get balance of reserve token") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Failed to get balance of reserve token' + }) + + return + end + LogActivity(msg.Tags['Action'], json.encode({ Balance = balanceResp.Data }), "Got balance of reserve token") + + if bint.__lt(bint(balanceResp.Data), bint(ReserveBalance)) then + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits), + Balance = balanceResp.Data, + ReserveBalance = ReserveBalance + }), + "Insufficient reserve balance to sell") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Insufficient reserve balance to sell', + ReserveBalance = tostring(ReserveBalance), + CurrentBalance = tostring(balanceResp.Data) + }) + + return + end + + if bint.__lt(bint(ReserveBalance), bint(cost)) then + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits), + Balance = balanceResp.Data, + ReserveBalance = ReserveBalance + }), + "Insufficient reserve balance to sell") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Insufficient reserve balance to sell', + ReserveBalance = tostring(ReserveBalance), + Cost = tostring(cost) + }) + + return + end + + local burnResp = ao.send({ Target = RepoToken.processId, Action = "Burn", Tags = { Quantity = tostring(tokensToSellInSubUnits), Recipient = msg.From } }) + .receive() + if burnResp.Tags['Action'] ~= 'Burn-Response' then + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits), + Balance = balanceResp.Data, + ReserveBalance = ReserveBalance + }), + "Failed to burn tokens") + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Failed to burn tokens. Amount will be refunded.' + }) + + return + end + + local transferResp = ao.send({ + Target = ReserveToken.processId, + Action = "Transfer", + Recipient = msg.From, + Quantity = + tostring(cost) + }).receive() + if transferResp.Tags['Action'] ~= 'Debit-Notice' then + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring( + tokensToSellInSubUnits), + Balance = balanceResp.Data, + ReserveBalance = ReserveBalance + }), + "Failed to transfer reserve tokens") + RefundsMap[msg.From] = tostring(cost) + msg.reply({ + Action = 'Sell-Tokens-Error', + Error = 'Failed to transfer reserve tokens. try again.' + }) + + return + end + + ReserveBalance = utils.subtract(ReserveBalance, cost) + + + + LogActivity(msg.Tags['Action'], + json.encode({ + Proceeds = tostring(cost), + CurrentSupply = currentSupplyResp.Data, + TokensToSell = tostring(tokensToSellInSubUnits), + Balance = balanceResp.Data, + ReserveBalance = ReserveBalance + }), + "Successfully sold tokens") + msg.reply({ + Action = 'Sell-Tokens-Response', + TokensSold = utils.toBalanceValue(tokensToSellInSubUnits), + Cost = tostring(cost), + Data = 'Successfully sold ' .. tokensToSell .. ' tokens' + }) + end + + return mod + +end + +_G.package.loaded["src.handlers.bonding_curve"] = _loaded_mod_src_handlers_bonding_curve() + +-- module: "src.utils.validations" +local function _loaded_mod_src_utils_validations() + local mod = {} + + local regexPatterns = { + uuid = "^[0-9a-fA-F]%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$", + address = "^[a-zA-Z0-9-_]+$", + email = "^%w+@[%w%.]+%.%w+$", + url = "^%w+://[%w%.]+%.%w+", + username = "^[a-zA-Z0-9]+$" + } + + -- Helper function for pattern matching + local function matches(input, pattern) + return string.match(input, pattern) ~= nil + end + + local function endsWith(str, ending) + return ending == "" or str:sub(- #ending) == ending + end + + -- Type checking functions + function mod.isUuid(input) + return type(input) == 'string' and matches(input, regexPatterns.uuid) + end + + function mod.isArweaveAddress(input) + return type(input) == 'string' and #input == 43 and matches(input, regexPatterns.address) + end + + function mod.isObject(input) + return type(input) == 'table' and not (getmetatable(input) or {}).__isarray + end + + function mod.isArray(input) + return type(input) == 'table' and (getmetatable(input) or {}).__isarray + end + + function mod.isEmail(input, skipEmptyStringCheck) + if skipEmptyStringCheck and input == '' then return true end + return type(input) == 'string' and matches(input, regexPatterns.email) + end + + function mod.isUsername(input) + return type(input) == 'string' and #input >= 4 and #input <= 39 and not endsWith(input, "-") and + matches(input, regexPatterns.username) + end + + function mod.isURL(input, skipEmptyStringCheck) + if skipEmptyStringCheck and input == '' then return true end + return type(input) == 'string' and matches(input, regexPatterns.url) + end + + -- Main type checking function + local function isType(input, expectedType, skipEmptyStringCheck) + if expectedType == 'object' then + return mod.isObject(input) + elseif expectedType == 'array' then + return mod.isArray(input) + elseif expectedType == 'uuid' then + return mod.isUuid(input) + elseif expectedType == 'arweave-address' then + return mod.isArweaveAddress(input) + elseif expectedType == 'url' then + return mod.isURL(input, skipEmptyStringCheck) + elseif expectedType == 'email' then + return mod.isEmail(input, skipEmptyStringCheck) + elseif expectedType == 'username' then + return mod.isUsername(input) + else + return type(input) == expectedType + end + end + + -- Validation function + function mod.isInvalidInput(input, expectedTypes, skipEmptyStringCheck) + skipEmptyStringCheck = skipEmptyStringCheck or false + if input == nil or (not skipEmptyStringCheck and input == '') then + return true + end + + if type(expectedTypes) ~= 'table' then expectedTypes = { expectedTypes } end + for _, expectedType in ipairs(expectedTypes) do + if isType(input, expectedType, skipEmptyStringCheck) then + return false + end + end + return true + end + + return mod + +end + +_G.package.loaded["src.utils.validations"] = _loaded_mod_src_utils_validations() + +-- module: "src.handlers.token_manager" +local function _loaded_mod_src_handlers_token_manager() + local aolibs = require "src.libs.aolibs" + local validations = require "src.utils.validations" + local utils = require "src.utils.mod" + local bint = require('.bint')(256) + local json = aolibs.json + local mod = {} + + --- @type RepoToken + RepoToken = RepoToken or nil + --- @type ReserveToken + ReserveToken = ReserveToken or nil + --- @type ReserveBalance + ReserveBalance = ReserveBalance or '0' + --- @type FundingGoal + FundingGoal = FundingGoal or '0' + --- @type AllocationForLP + AllocationForLP = AllocationForLP or '0' + --- @type AllocationForCreator + AllocationForCreator = AllocationForCreator or '0' + --- @type SupplyToSell + SupplyToSell = SupplyToSell or '0' + --- @type MaxSupply + MaxSupply = MaxSupply or '0' + --- @type Initialized + Initialized = Initialized or false + --- @type ReachedFundingGoal + ReachedFundingGoal = ReachedFundingGoal or false + --- @type LiquidityPool + LiquidityPool = LiquidityPool or nil + --- @type Creator + Creator = Creator or nil + + ---@type HandlerFunction + function mod.initialize(msg) + assert(Initialized == false, "TokenManager already initialized") + assert(msg.Data ~= nil, "Data is required") + + --- @type CBTMInitPayload + local initPayload = json.decode(msg.Data) + + if ( + validations.isInvalidInput(initPayload, 'object') or + validations.isInvalidInput(initPayload.repoToken, 'object') or + validations.isInvalidInput(initPayload.repoToken.tokenName, 'string') or + validations.isInvalidInput(initPayload.repoToken.tokenTicker, 'string') or + validations.isInvalidInput(initPayload.repoToken.denomination, 'string') or + validations.isInvalidInput(initPayload.repoToken.tokenImage, 'string') or + validations.isInvalidInput(initPayload.repoToken.processId, 'string') or + validations.isInvalidInput(initPayload.reserveToken, 'object') or + validations.isInvalidInput(initPayload.reserveToken.tokenName, 'string') or + validations.isInvalidInput(initPayload.reserveToken.tokenTicker, 'string') or + validations.isInvalidInput(initPayload.reserveToken.denomination, 'string') or + validations.isInvalidInput(initPayload.reserveToken.tokenImage, 'string') or + validations.isInvalidInput(initPayload.reserveToken.processId, 'string') or + validations.isInvalidInput(initPayload.fundingGoal, 'string') or + validations.isInvalidInput(initPayload.allocationForLP, 'string') or + validations.isInvalidInput(initPayload.allocationForCreator, 'string') or + validations.isInvalidInput(initPayload.maxSupply, 'string') + ) then + if msg.reply then + msg.reply({ + Action = 'Initialize-Error', + Error = 'Invalid inputs supplied.' + }) + return + else + ao.send({ + Target = msg.From, + Action = 'Initialize-Error', + Error = 'Invalid inputs supplied.' + }) + end + end + + local lpAllocation = utils.udivide(utils.multiply(initPayload.maxSupply, "20"), "100") + + local supplyToSell = utils.subtract(initPayload.maxSupply, + utils.add(lpAllocation, initPayload.allocationForCreator)) + + if (bint(supplyToSell) <= 0) then + if msg.reply then + msg.reply({ + Action = 'Initialize-Error', + Error = 'Pre-Allocations and Dex Allocations exceeds max supply' + }) + return + else + ao.send({ + Target = msg.From, + Action = 'Initialize-Error', + Error = 'Pre-Allocations and Dex Allocations exceeds max supply' + }) + return + end + end + + RepoToken = initPayload.repoToken + ReserveToken = initPayload.reserveToken + FundingGoal = utils.toBalanceValue(utils.toSubUnits(initPayload.fundingGoal, ReserveToken.denomination)) + AllocationForLP = utils.toBalanceValue(utils.toSubUnits(lpAllocation, RepoToken.denomination)) + AllocationForCreator = utils.toBalanceValue(utils.toSubUnits(initPayload.allocationForCreator, RepoToken + .denomination)) + MaxSupply = utils.toBalanceValue(utils.toSubUnits(initPayload.maxSupply, RepoToken.denomination)) + SupplyToSell = utils.toBalanceValue(utils.toSubUnits(supplyToSell, RepoToken.denomination)) + Creator = msg.From + + Initialized = true + + msg.reply({ + Action = 'Initialize-Response', + Initialized = true, + }) + end + + ---@type HandlerFunction + function mod.info(msg) + msg.reply({ + Action = 'Info-Response', + Data = json.encode({ + reserveBalance = ReserveBalance, + initialized = Initialized, + repoToken = RepoToken, + reserveToken = ReserveToken, + fundingGoal = FundingGoal, + allocationForLP = AllocationForLP, + allocationForCreator = AllocationForCreator, + maxSupply = MaxSupply, + supplyToSell = SupplyToSell, + reachedFundingGoal = ReachedFundingGoal, + liquidityPool = LiquidityPool, + creator = Creator + }) + }) + end + + return mod + +end + +_G.package.loaded["src.handlers.token_manager"] = _loaded_mod_src_handlers_token_manager() + +-- module: "src.handlers.liquidity_pool" +local function _loaded_mod_src_handlers_liquidity_pool() + local utils = require('src.utils.mod') + local mod = {} + + function mod.depositToLiquidityPool(msg) + assert(msg.From == Creator, "Only the creator can make liquidity pool requests") + assert(LiquidityPool == nil, "Liquidity pool already initialized") + assert(ReachedFundingGoal == true, "Funding goal not reached") + + local poolId = msg.Tags['Pool-Id'] + assert(type(poolId) == 'string', "Pool ID is required") + + -- local mintQty = utils.divide(utils.multiply(MaxSupply, "20"), "100") + local mintResponse = ao.send({ + Target = RepoToken.processId, Action = "Mint", Quantity = AllocationForLP + }).receive() + + if mintResponse.Tags['Action'] ~= 'Mint-Response' then + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Error', + Error = 'Failed to mint tokens.' + }) + + return + end + + local balanceResponseRepoToken = ao.send({ + Target = RepoToken.processId, Action = "Balance" + }).receive() + + local tokenAQty = balanceResponseRepoToken.Data + + if tokenAQty == nil or tokenAQty == "0" then + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Error', + Error = "No repo tokens to deposit", + }) + return + end + + local balanceResponseReserveToken = ao.send({ + Target = ReserveToken.processId, Action = "Balance" + }).receive() + + local tokenBQty = balanceResponseReserveToken.Data + + if tokenBQty == nil or tokenBQty == "0" then + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Error', + Error = "No reserve tokens to deposit", + }) + return + end + + local tokenADepositResponse = ao.send({ + Target = RepoToken.processId, + Action = "Transfer", + Quantity = tokenAQty, + Recipient = poolId, + ["X-Action"] = "Provide", + ["X-Slippage-Tolerance"] = "0.5" + }).receive() + + if tokenADepositResponse.Tags['Action'] ~= 'Debit-Notice' then + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Error', + Error = 'Failed to transfer Repo tokens to LP. try again.' + }) + + return + end + + local tokenBDepositResponse = ao.send({ + Target = ReserveToken.processId, + Action = "Transfer", + Quantity = tokenBQty, + Recipient = poolId, + ["X-Action"] = "Provide", + ["X-Slippage-Tolerance"] = "0.5" + }).receive() + + if tokenBDepositResponse.Tags['Action'] ~= 'Debit-Notice' then + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Error', + Error = 'Failed to transfer Repo tokens to LP. try again.' + }) + + return + end + + LiquidityPool = poolId + + --Check reserves of the pool + + msg.reply({ + Action = 'Deposit-To-Liquidity-Pool-Response', + ["Pool-Id"] = poolId, + ["Status"] = "Success" + }) + end + + return mod + +end + +_G.package.loaded["src.handlers.liquidity_pool"] = _loaded_mod_src_handlers_liquidity_pool() + +local bondingCurve = require "src.handlers.bonding_curve" +local tokenManager = require "src.handlers.token_manager" +local liquidityPool = require "src.handlers.liquidity_pool" + +Handlers.add('Initialize-Bonding-Curve', Handlers.utils.hasMatchingTag('Action', 'Initialize-Bonding-Curve'), + tokenManager.initialize) + +Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), + tokenManager.info) + +Handlers.add('Get-Buy-Price', Handlers.utils.hasMatchingTag('Action', 'Get-Buy-Price'), + bondingCurve.getBuyPrice) + +Handlers.add('Get-Sell-Price', Handlers.utils.hasMatchingTag('Action', 'Get-Sell-Price'), + bondingCurve.getSellPrice) + +Handlers.add('Sell-Tokens', Handlers.utils.hasMatchingTag('Action', 'Sell-Tokens'), + bondingCurve.sellTokens) + + +Handlers.add('Deposit-To-Liquidity-Pool', Handlers.utils.hasMatchingTag('Action', 'Deposit-To-Liquidity-Pool'), + liquidityPool.depositToLiquidityPool) + +Handlers.add( + "Buy-Tokens", + { Action = "Credit-Notice", ["X-Action"] = "Buy-Tokens" }, + bondingCurve.buyTokens) \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index c3fcba10..03ca2506 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,7 +17,7 @@ import Repository from './pages/repository/Repository' import RepositoryWrapper from './pages/repository/RepositoryWrapper' const repositoryRoutes = [ - { path: '/repository/:id/:tabName?/*?', element: }, + { path: '/repository/:id/:tabName?/*?/:settingsTabName?', element: }, { path: '/repository/:id/pull/new', element: }, { path: '/repository/:id/pull/:pullId', element: }, { path: '/repository/:id/issue/new', element: }, diff --git a/src/assets/coin-minting-loading.json b/src/assets/coin-minting-loading.json new file mode 100644 index 00000000..28a84416 --- /dev/null +++ b/src/assets/coin-minting-loading.json @@ -0,0 +1 @@ +{"nm":"Manufacturing Capability","ddd":0,"h":200,"w":200,"meta":{"g":"@lottiefiles/toolkit-js 0.33.2"},"layers":[{"ty":4,"nm":"Bottom Square","sr":1,"st":0,"op":95,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-49.25,50.75,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[57.75,132,0],"t":5,"ti":[-11.333,0,0],"to":[11.333,0,0]},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[125.75,132,0],"t":29,"ti":[0,0,0],"to":[0,0,0]},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[125.75,132,0],"t":42,"ti":[11.333,0,0],"to":[-11.333,0,0]},{"s":[57.75,132,0],"t":68}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":4,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"s":{"a":0,"k":[24,24],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Kobalt Blue","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.4902,0.5255,0.6118],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-49.25,50.75],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Bottom Line","sr":1,"st":0,"op":95,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-6.313,51.844,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[111,133.625,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[{"ty":28,"mn":"ADBE Set Matte3","nm":"Set Matte","ix":1,"en":1,"ef":[{"ty":10,"mn":"ADBE Set Matte3-0001","nm":"Take Matte From Layer","ix":1,"v":{"a":0,"k":11,"ix":1}},{"ty":7,"mn":"ADBE Set Matte3-0002","nm":"Use For Matte","ix":2,"v":{"a":0,"k":4,"ix":2}},{"ty":7,"mn":"ADBE Set Matte3-0003","nm":"Invert Matte","ix":3,"v":{"a":0,"k":1,"ix":3}},{"ty":7,"mn":"ADBE Set Matte3-0004","nm":"If Layer Sizes Differ","ix":4,"v":{"a":0,"k":1,"ix":4}},{"ty":7,"mn":"ADBE Set Matte3-0005","nm":"Composite Matte with Original","ix":5,"v":{"a":0,"k":1,"ix":5}},{"ty":7,"mn":"ADBE Set Matte3-0006","nm":"Premultiply Matte Layer","ix":6,"v":{"a":0,"k":1,"ix":6}}]}],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":4,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-88.344,51.656],[52.656,51.656]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Kobalt Blue","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.4902,0.5255,0.6118],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2},{"ty":4,"nm":"Circle","sr":1,"st":0,"op":95,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[41.5,-0.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[123.5,99.5,0],"t":5,"ti":[11.167,0,0],"to":[-11.167,0,0]},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[56.5,99.5,0],"t":27,"ti":[0,0,0],"to":[0,0,0]},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[56.5,99.5,0],"t":37,"ti":[-11.167,0,0],"to":[11.167,0,0]},{"s":[123.5,99.5,0],"t":58}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":4,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[22,22],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Orange","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.3373,0.6784,0.851],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[41.5,-0.5],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3},{"ty":4,"nm":"Center Line","sr":1,"st":0,"op":95,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-1,1.125,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[99,101.125,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[{"ty":28,"mn":"ADBE Set Matte3","nm":"Set Matte","ix":1,"en":1,"ef":[{"ty":10,"mn":"ADBE Set Matte3-0001","nm":"Take Matte From Layer","ix":1,"v":{"a":0,"k":11,"ix":1}},{"ty":7,"mn":"ADBE Set Matte3-0002","nm":"Use For Matte","ix":2,"v":{"a":0,"k":4,"ix":2}},{"ty":7,"mn":"ADBE Set Matte3-0003","nm":"Invert Matte","ix":3,"v":{"a":0,"k":1,"ix":3}},{"ty":7,"mn":"ADBE Set Matte3-0004","nm":"If Layer Sizes Differ","ix":4,"v":{"a":0,"k":1,"ix":4}},{"ty":7,"mn":"ADBE Set Matte3-0005","nm":"Composite Matte with Original","ix":5,"v":{"a":0,"k":1,"ix":5}},{"ty":7,"mn":"ADBE Set Matte3-0006","nm":"Premultiply Matte Layer","ix":6,"v":{"a":0,"k":1,"ix":6}}]}],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":4,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[69.938,-0.125],[-71.047,-0.25]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Kobalt Blue","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.4902,0.5255,0.6118],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":4},{"ty":4,"nm":"Top Square","sr":1,"st":0,"op":95,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-49.5,-52.25,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[59.5,68,0],"t":12,"ti":[-10.917,0,0],"to":[10.917,0,0]},{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[125,68,0],"t":39,"ti":[0,0,0],"to":[0,0,0]},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[125,68,0],"t":45,"ti":[10.917,0,0],"to":[-10.917,0,0]},{"s":[59.5,68,0],"t":74}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Rectangle 1","ix":1,"cix":2,"np":4,"it":[{"ty":"rc","bm":0,"hd":false,"mn":"ADBE Vector Shape - Rect","nm":"Rectangle Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"s":{"a":0,"k":[24,24.279],"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Kobalt Blue","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.4902,0.5255,0.6118],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-49.5,-52.25],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":5},{"ty":4,"nm":"Top Line","sr":1,"st":0,"op":95,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-1.75,-50.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[98.25,68,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[{"ty":28,"mn":"ADBE Set Matte3","nm":"Set Matte","ix":1,"en":1,"ef":[{"ty":10,"mn":"ADBE Set Matte3-0001","nm":"Take Matte From Layer","ix":1,"v":{"a":0,"k":11,"ix":1}},{"ty":7,"mn":"ADBE Set Matte3-0002","nm":"Use For Matte","ix":2,"v":{"a":0,"k":4,"ix":2}},{"ty":7,"mn":"ADBE Set Matte3-0003","nm":"Invert Matte","ix":3,"v":{"a":0,"k":1,"ix":3}},{"ty":7,"mn":"ADBE Set Matte3-0004","nm":"If Layer Sizes Differ","ix":4,"v":{"a":0,"k":1,"ix":4}},{"ty":7,"mn":"ADBE Set Matte3-0005","nm":"Composite Matte with Original","ix":5,"v":{"a":0,"k":1,"ix":5}},{"ty":7,"mn":"ADBE Set Matte3-0006","nm":"Premultiply Matte Layer","ix":6,"v":{"a":0,"k":1,"ix":6}}]}],"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Shape 1","ix":1,"cix":2,"np":4,"it":[{"ty":"sh","bm":0,"hd":false,"mn":"ADBE Vector Shape - Group","nm":"Path 1","ix":1,"d":1,"ks":{"a":0,"k":{"c":true,"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-71.094,-50.5],[69.969,-50.5]]},"ix":2}},{"ty":"st","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Stroke","nm":"Kobalt Blue","lc":1,"lj":1,"ml":4,"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"c":{"a":0,"k":[0.4902,0.5255,0.6118],"ix":3}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,1,1],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[0,0],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":6},{"ty":1,"nm":"Background","sr":1,"st":0,"op":95,"ip":0,"hd":true,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[100,100,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6},"sk":{"a":0,"k":0},"p":{"a":0,"k":[100,100,0],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":0,"k":100,"ix":11}},"ef":[],"sc":"#ffffff","sh":200,"sw":200,"ind":7}],"v":"5.5.8","fr":30,"op":76,"ip":0,"assets":[]} \ No newline at end of file diff --git a/src/components/common/buttons/Button.tsx b/src/components/common/buttons/Button.tsx index 4ebd13e7..5ddf01db 100644 --- a/src/components/common/buttons/Button.tsx +++ b/src/components/common/buttons/Button.tsx @@ -9,7 +9,7 @@ const VARIANTS = { 'text-white text-sm md:text-base font-medium font-inter leading-normal shadow-[0px_2px_4px_0px_rgba(0,0,0,0.05)] border border-white border-opacity-50 bg-gradient-dark hover:bg-gradient-dark-hover hover:text-gray-300 hover:border-opacity-40', outline: 'text-[#4388f6] hover:bg-[#4388f6] hover:text-white border-[1.2px] border-[#4388f6]', ghost: 'hover:bg-[#c6dcff] text-[#4388f6] rounded-[8px]', - link: '!px-0 !pb-1 text-[#4388f6]', + link: '!px-0 !pb-1 text-[#4388f6] disabled:text-[#A6A6A6] disabled:bg-transparent disabled:shadow-none', solid: 'bg-[#4388f6] text-base tracking-wide text-white', 'primary-solid': 'bg-primary-600 text-white hover:bg-primary-500 shadow-[0px_2px_4px_0px_rgba(0,0,0,0.05)] active:bg-primary-700 active:shadow-[0px_2px_6px_0px_rgba(0,0,0,0.05)]', diff --git a/src/helpers/constants.ts b/src/helpers/constants.ts index e4799fc5..11ee1a1c 100644 --- a/src/helpers/constants.ts +++ b/src/helpers/constants.ts @@ -1,4 +1,4 @@ -export const AOS_PROCESS_ID = 'yJZ3_Yrc-qYRt1zHmY7YeNvpmQwuqyK3dT0-gxWftew' +export const AOS_PROCESS_ID = 'bZWnqbj9VUP5p7PVKxoMlej8BHbSwNC2iQQoEFhND8Q' export const VITE_GA_TRACKING_ID = 'G-L433HSR0D0' export const AMPLITUDE_TRACKING_ID = '92a463755ed8c8b96f0f2353a37b7b2' export const PL_REPO_ID = '6ace6247-d267-463d-b5bd-7e50d98c3693' diff --git a/src/helpers/getArrayBufSize.ts b/src/helpers/getArrayBufSize.ts index 5c7d7f4e..5695e52f 100644 --- a/src/helpers/getArrayBufSize.ts +++ b/src/helpers/getArrayBufSize.ts @@ -1,5 +1,13 @@ -export function getArrayBufSize(arrayBuffer: ArrayBuffer): GetArrayBufSizeReturnType { - const byteSize = arrayBuffer.byteLength +export function getRepoSize(arrayBufferOrSize: ArrayBuffer | number): GetArrayBufSizeReturnType { + let byteSize = 0 + + if (arrayBufferOrSize instanceof ArrayBuffer) { + byteSize = arrayBufferOrSize.byteLength + } + + if (typeof arrayBufferOrSize === 'number') { + byteSize = arrayBufferOrSize + } if (byteSize >= 1073741824) { return { diff --git a/src/helpers/imgUrlFormatter.ts b/src/helpers/imgUrlFormatter.ts new file mode 100644 index 00000000..9144336b --- /dev/null +++ b/src/helpers/imgUrlFormatter.ts @@ -0,0 +1,7 @@ +export function imgUrlFormatter(url: string) { + if (url.match(/^[a-zA-Z0-9_-]{43}$/)) { + return `https://arweave.net/${url}` + } + + return url +} diff --git a/src/helpers/parseScientific.ts b/src/helpers/parseScientific.ts new file mode 100644 index 00000000..da23307c --- /dev/null +++ b/src/helpers/parseScientific.ts @@ -0,0 +1,32 @@ +export function parseScientific(num: string): string { + // If the number is not in scientific notation return it as it is. + if (!/\d+\.?\d*e[+-]*\d+/i.test(num)) { + return num + } + + // Remove the sign. + const numberSign = Math.sign(Number(num)) + num = Math.abs(Number(num)).toString() + + // Parse into coefficient and exponent. + const [coefficient, exponent] = num.toLowerCase().split('e') + let zeros = Math.abs(Number(exponent)) + const exponentSign = Math.sign(Number(exponent)) + const [integer, decimals] = (coefficient.indexOf('.') != -1 ? coefficient : `${coefficient}.`).split('.') + + if (exponentSign === -1) { + zeros -= integer.length + num = + zeros < 0 + ? integer.slice(0, zeros) + '.' + integer.slice(zeros) + decimals + : '0.' + '0'.repeat(zeros) + integer + decimals + } else { + if (decimals) zeros -= decimals.length + num = + zeros < 0 + ? integer + decimals.slice(0, zeros) + '.' + decimals.slice(zeros) + : integer + decimals + '0'.repeat(zeros) + } + + return numberSign < 0 ? '-' + num : num +} diff --git a/src/helpers/wallet/fetchAllTokens.ts b/src/helpers/wallet/fetchAllTokens.ts new file mode 100644 index 00000000..4c6edc1e --- /dev/null +++ b/src/helpers/wallet/fetchAllTokens.ts @@ -0,0 +1,15 @@ +export async function fetchAllUserTokens() { + const tokens = await (window.arweaveWallet as any).userTokens({ fetchBalance: false }) + const aoPIDMirror = 'Pi-WmAQp2-mh-oWH9lWpz5EthlUDj_W0IusAv-RXhRk' + const aoOriginalPID = 'm3PaWzK4PTG9lAaqYQPaPdOcXdO8hYqi5Fe9NWqXd0w' + + const formattedTokens = tokens.map((token: any) => ({ + tokenName: token.Name, + tokenTicker: token.Ticker, + tokenImage: token.Logo, + processId: token.processId === aoOriginalPID ? aoPIDMirror : token.processId, + denomination: token.Denomination.toString() + })) + + return formattedTokens +} diff --git a/src/lib/arfs/arfsSingleton.ts b/src/lib/arfs/arfsSingleton.ts new file mode 100644 index 00000000..078d833a --- /dev/null +++ b/src/lib/arfs/arfsSingleton.ts @@ -0,0 +1,37 @@ +import { ArFS, BiFrost, Drive } from 'arfs-js' + +export class ArFSSingleton { + driveInstance: Drive | null = null + bifrostInstance: BiFrost | null = null + arfsInstance: ArFS | null = null + + constructor() {} + + getInstance() { + return this + } + + getBifrostInstance() { + return this.bifrostInstance + } + + getArfsInstance() { + return this.arfsInstance + } + + getDriveInstance() { + return this.driveInstance + } + + setDrive(drive: Drive) { + this.driveInstance = drive + } + + setBifrost(bifrost: BiFrost) { + this.bifrostInstance = bifrost + } + + setArFS(arfs: ArFS) { + this.arfsInstance = arfs + } +} diff --git a/src/lib/arfs/arfsSingletonMap.ts b/src/lib/arfs/arfsSingletonMap.ts new file mode 100644 index 00000000..e969df7f --- /dev/null +++ b/src/lib/arfs/arfsSingletonMap.ts @@ -0,0 +1,38 @@ +import { ArFSSingleton } from './arfsSingleton' + +let instance: ArFSSingletonMap +const map: Map = new Map() + +export class ArFSSingletonMap { + constructor() { + if (instance) { + throw new Error('You can only create one instance!') + } + // eslint-disable-next-line @typescript-eslint/no-this-alias + instance = this + } + + getInstance() { + return this + } + + getArFSSingleton(key: string) { + if (!map.has(key)) { + throw new Error('Singleton Instance not found.') + } + + return map.get(key) + } + + setArFSSingleton(key: string, arfsSingleton: ArFSSingleton) { + map.set(key, arfsSingleton) + } + + getAllArFSSingletons() { + return map + } +} + +const arfsSingletonMap = Object.freeze(new ArFSSingletonMap()) + +export default arfsSingletonMap diff --git a/src/lib/arfs/arfsTxSubmissionOverride.ts b/src/lib/arfs/arfsTxSubmissionOverride.ts new file mode 100644 index 00000000..4aab8f62 --- /dev/null +++ b/src/lib/arfs/arfsTxSubmissionOverride.ts @@ -0,0 +1,24 @@ +import Transaction from 'arweave/web/lib/transaction' +import { v4 as uuidv4 } from 'uuid' + +import { createSignedQueuePayload } from '../queue/helpers' +import taskQueueSingleton from '../queue/TaskQueue' + +export async function arfsTxSubmissionOverride(txList: Transaction[]) { + const queueStatus = taskQueueSingleton.getTaskQueueStatus() + const txIds: string[] = [] + + if (queueStatus === 'Busy') throw new Error('Task Queue is busy. Try again later.') + + for (const tx of txList) { + const dataItem = await createSignedQueuePayload(tx) + + const token = uuidv4() + taskQueueSingleton.sendToPending(token, dataItem) + + const txid = await dataItem.id + txIds.push(txid) + } + + return { successTxIds: txIds, failedTxIndex: [] } +} diff --git a/src/lib/arfs/getArFS.ts b/src/lib/arfs/getArFS.ts new file mode 100644 index 00000000..81461aac --- /dev/null +++ b/src/lib/arfs/getArFS.ts @@ -0,0 +1,10 @@ +import { ArFS } from 'arfs-js' + +import { arfsTxSubmissionOverride } from './arfsTxSubmissionOverride' + +export function getArFS() { + const arfs = new ArFS({ wallet: 'use_wallet', appName: 'Protocol.Land' }) + arfs.api.signAndSendAllTransactions = arfsTxSubmissionOverride + + return arfs +} diff --git a/src/lib/arfs/getBifrost.ts b/src/lib/arfs/getBifrost.ts new file mode 100644 index 00000000..aeb9033b --- /dev/null +++ b/src/lib/arfs/getBifrost.ts @@ -0,0 +1,7 @@ +import { ArFS, BiFrost, Drive } from 'arfs-js' + +export function getBifrost(drive: Drive, arfs: ArFS) { + const bifrost = new BiFrost(drive, arfs) + + return bifrost +} diff --git a/src/lib/bark/index.ts b/src/lib/bark/index.ts new file mode 100644 index 00000000..e68dc2ba --- /dev/null +++ b/src/lib/bark/index.ts @@ -0,0 +1,107 @@ +import { dryrun } from '@permaweb/aoconnect' + +import { getTags } from '@/helpers/getTags' +import { isArweaveAddress } from '@/helpers/isInvalidInput' +import { CreateLiquidityPoolProps } from '@/pages/repository/components/decentralize-modals/config' +import { RepoLiquidityPoolToken } from '@/types/repository' + +import { sendMessage } from '../contract' +import { pollForTxBeingAvailable, pollLiquidityPoolMessages } from '../decentralize' + +// export const AMM_PROCESS_ID = 'gCx8TjISuSqRTXXVab22plluBy5S0YyvxPYU5xqFqc8' +export const AMM_PROCESS_ID = '3XBGLrygs11K63F_7mldWz4veNx6Llg6hI2yZs8LKHo' + +export async function checkIfLiquidityPoolExists(tokenA: RepoLiquidityPoolToken, tokenB: RepoLiquidityPoolToken) { + const args = { + tags: getTags({ + Action: 'Get-Pool', + 'Token-A': tokenA.processId, + 'Token-B': tokenB.processId + }), + process: AMM_PROCESS_ID + } + + const { Messages } = await dryrun(args) + + if (Messages.length > 0) { + return true + } + + return false +} + +export async function checkLiquidityPoolReserves(poolId: string) { + const args = { + tags: getTags({ + Action: 'Get-Reserves' + }), + process: poolId + } + + let attempts = 0; + while (attempts < 10) { + const { Messages } = await dryrun(args) + + if (!Messages[0]) { + attempts++; + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for 1 second before retrying + continue; + } + + let data: Record = {} + + data = Messages[0].Tags.reduce((acc: any, curr: any) => { + if (isArweaveAddress(curr.name)) { + acc[curr.name] = curr.value + } + return acc + }, {}) + + if (Object.keys(data).length >= 2) { + return data; + } + + attempts++; + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for 1 second before retrying + } + + return null; // Return null if we couldn't get at least two entries after 10 attempts +} + +export async function depositToLiquidityPool(poolId: string, token: RepoLiquidityPoolToken, amount: string) { + const args = { + tags: getTags({ + Action: 'Transfer', + Recipient: poolId, + Quantity: (+amount * 10 ** +token.denomination).toString(), + 'X-Slippage-Tolerance': '0.5', + 'X-Action': 'Provide' + }), + pid: token.processId + } + + const msgId = await sendMessage(args) + + return msgId +} + +export async function createLiquidityPool(payload: CreateLiquidityPoolProps) { + const { tokenA, tokenB } = payload + + const args = { + tags: getTags({ + Action: 'Add-Pool', + 'Token-A': tokenA.processId, + 'Token-B': tokenB.processId, + Name: `Bark v2 Pool ${tokenA.processId.slice(0, 6)}...${tokenB.processId.slice(-6)}` + }), + pid: AMM_PROCESS_ID + } + + const msgId = await sendMessage(args) + await pollForTxBeingAvailable({ txId: msgId }) + + const poolStatus = await pollLiquidityPoolMessages(msgId) + + return poolStatus +} \ No newline at end of file diff --git a/src/lib/bonding-curve/buy.ts b/src/lib/bonding-curve/buy.ts new file mode 100644 index 00000000..945ea12a --- /dev/null +++ b/src/lib/bonding-curve/buy.ts @@ -0,0 +1,89 @@ +import { getTags } from '@/helpers/getTags' + +import { sendMessage } from '../contract' +import { pollForTxBeingAvailable } from '../decentralize' +import { pollBuyTokensMessages } from '.' +import { N_PLUS_1 } from './constants' +import { getTokenCurrentSupply } from './helpers' + +export async function calculateTokensBuyCost( + repoTokenPID: string, + tokensToBuy: number, + maxSupply: number, + fundingGoal: number +): Promise { + const currentSupply = await getTokenCurrentSupply(repoTokenPID) + const s1 = parseInt(currentSupply) + const s2 = s1 + tokensToBuy + + if (s2 > maxSupply) { + throw new Error('Cannot exceed maximum supply') + } + + const S_exp = Math.pow(maxSupply, N_PLUS_1) + + if (S_exp <= 0) { + throw new Error('S_exp must be greater than zero') + } + + // Cost = G * [ (s2)^(n+1) - (s1)^(n+1) ] / S^(n+1) + const s1_exp = Math.pow(s1, N_PLUS_1) + const s2_exp = Math.pow(s2, N_PLUS_1) + + const numerator = fundingGoal * (s2_exp - s1_exp) + const cost = numerator / S_exp + + return cost +} + +export async function buyTokens(bondingCurvePID: string, reserveTokenPID: string, tokensToBuy: string, cost: string) { + console.log('buyTokens', bondingCurvePID, tokensToBuy, cost) + + const args = { + tags: getTags({ + 'X-Action': 'Buy-Tokens', + 'X-Token-Quantity': tokensToBuy, + Action: 'Transfer', + Quantity: cost, + Recipient: bondingCurvePID + }), + pid: reserveTokenPID + } + + const msgId = await sendMessage(args) + await pollForTxBeingAvailable({ txId: msgId }) + + const { success, message } = await pollBuyTokensMessages(msgId) + return { success, message } +} + +export async function calculateTokensToSell( + repoTokenPID: string, + tokensToSell: number, + maxSupply: number, + fundingGoal: number +): Promise { + const currentSupply = await getTokenCurrentSupply(repoTokenPID) + + const s1 = currentSupply + const s2 = currentSupply - tokensToSell + + if (s2 < 0) { + throw new Error('Cannot sell more tokens than current supply') + } + + const S_exp = Math.pow(maxSupply, N_PLUS_1) + + if (S_exp <= 0) { + throw new Error('S_exp must be greater than zero') + } + + // Reserve received = G * [ (s1)^(n+1) - (s2)^(n+1) ] / S^(n+1) + const s1_exp = Math.pow(s1, N_PLUS_1) + const s2_exp = Math.pow(s2, N_PLUS_1) + + const numerator = fundingGoal * (s1_exp - s2_exp) + const reserveReceived = numerator / S_exp + + return reserveReceived +} diff --git a/src/lib/bonding-curve/constants.ts b/src/lib/bonding-curve/constants.ts new file mode 100644 index 00000000..96807d5c --- /dev/null +++ b/src/lib/bonding-curve/constants.ts @@ -0,0 +1,2 @@ +export const N = 2 +export const N_PLUS_1 = N + 1 \ No newline at end of file diff --git a/src/lib/bonding-curve/helpers.ts b/src/lib/bonding-curve/helpers.ts new file mode 100644 index 00000000..8be8f4d1 --- /dev/null +++ b/src/lib/bonding-curve/helpers.ts @@ -0,0 +1,56 @@ +import { dryrun } from '@permaweb/aoconnect' + +import { getTags } from '@/helpers/getTags' +import { CurveState } from '@/stores/repository-core/types' + +export async function getTokenCurrentSupply(tokenId: string) { + const args = { + tags: getTags({ + Action: 'Total-Supply' + }), + process: tokenId + } + + const { Messages } = await dryrun(args) + const msg = Messages[0] + + if (!msg) { + throw new Error('Failed to get token current supply') + } + + const actionValue = msg.Tags.find((tag: any) => tag.name === 'Action')?.value + + if (actionValue !== 'Total-Supply-Response') { + throw new Error('Failed to get token current supply') + } + + const supply = msg.Data + + return supply +} + +export async function getCurveState(curveId: string) { + const args = { + tags: getTags({ + Action: 'Info' + }), + process: curveId + } + + const { Messages } = await dryrun(args) + const msg = Messages[0] + + if (!msg) { + throw new Error('Failed to get curve state') + } + + const actionValue = msg.Tags.find((tag: any) => tag.name === 'Action')?.value as string + + if (actionValue !== 'Info-Response') { + throw new Error('Failed to get curve state') + } + + const state = JSON.parse(msg.Data) as CurveState + + return state +} \ No newline at end of file diff --git a/src/lib/bonding-curve/index.ts b/src/lib/bonding-curve/index.ts new file mode 100644 index 00000000..26488668 --- /dev/null +++ b/src/lib/bonding-curve/index.ts @@ -0,0 +1,404 @@ +import { dryrun } from '@permaweb/aoconnect' +import { arGql, GQLUrls } from 'ar-gql' +import Arweave from 'arweave' + +import { getTags } from '@/helpers/getTags' +import { waitFor } from '@/helpers/waitFor' + +import { sendMessage } from '../contract' +import { pollForTxBeingAvailable } from '../decentralize' + +const arweave = new Arweave({ + host: 'arweave-search.goldsky.com', + port: 443, + protocol: 'https' +}) +const goldsky = arGql({ endpointUrl: GQLUrls.goldsky }) +export async function getTokenBuyPrice(amount: string, currentSupply: string, cruveId: string) { + const args = { + tags: getTags({ + Action: 'Get-Buy-Price', + 'Token-Quantity': amount, + 'Current-Supply': currentSupply + }), + process: cruveId + } + const { Messages } = await dryrun(args) + + if (!Messages || !Messages[0]) { + throw new Error('Failed to get token buy price') + } + + const cost = Messages[0].Data + + if (!cost) { + throw new Error('Failed to get token buy price') + } + + return parseInt(cost) +} + +const GET_BUY_PRICE_RESPONSE_ACTION = 'Get-Buy-Price-Response' +const GET_BUY_PRICE_ERROR_ACTION = 'Get-Buy-Price-Error' + +export async function pollGetBuyPriceMessages(msgId: string) { + const pollingOptions = { + maxAttempts: 50, + pollingIntervalMs: 3_000, + initialBackoffMs: 7_000 + } + const { maxAttempts, pollingIntervalMs, initialBackoffMs } = pollingOptions + + console.log('Polling for transaction...', { msgId }) + await waitFor(initialBackoffMs) + + let attempts = 0 + while (attempts < maxAttempts) { + let transaction + attempts++ + + try { + const response = await arweave.api.post('/graphql', { + query: + 'query ($messageId: String!, $limit: Int!, $sortOrder: SortOrder!, $cursor: String) {\n transactions(\n sort: $sortOrder\n first: $limit\n after: $cursor\n tags: [{name: \n"Pushed-For", values: [$messageId]}]\n ) {\n count\n ...MessageFields\n }\n}\nfragment MessageFields on TransactionConnection {\n edges {\n cursor\n node {\n id\n ingested_at\n recipient\n block {\n timestamp\n height\n }\n tags {\n name\n value\n }\n data {\n size\n }\n owner {\n address\n }\n }\n }\n}', + variables: { + messageId: msgId, + limit: 100, + sortOrder: 'INGESTED_AT_ASC', + cursor: '' + } + }) + + transaction = response?.data?.data?.transactions + console.log({ transaction }) + } catch (err) { + // Continue retries when request errors + console.log('Failed to poll for transaction...', { err }) + } + + if (transaction) { + const messages = transaction.edges.map((edge: any) => edge.node) + + for (const msg of messages) { + const getBuyPriceErrorMessage = msg.tags.find( + (tag: any) => tag.name === 'Action' && tag.value === GET_BUY_PRICE_ERROR_ACTION + ) + + if (getBuyPriceErrorMessage) { + const errMessage = msg.tags.find((tag: any) => tag.name === 'Error')?.value + + return { cost: null, message: errMessage } + } + + const getBuyPriceResponseMessage = msg.tags.find( + (tag: any) => tag.name === 'Action' && tag.value === GET_BUY_PRICE_RESPONSE_ACTION + ) + + if (getBuyPriceResponseMessage) { + const price = msg.tags.find((tag: any) => tag.name === 'Price')?.value + console.log({ price }) + return { cost: price } + } + } + } + console.log('Transaction not found...', { + msgId, + attempts, + maxAttempts, + pollingIntervalMs + }) + await waitFor(pollingIntervalMs) + } + + throw new Error('Transaction not found after polling, transaction id: ' + msgId) +} + +const BUY_TOKENS_RESPONSE_ACTION = 'Buy-Tokens-Response' +const BUY_TOKENS_ERROR_ACTION = 'Buy-Tokens-Error' + +export async function pollBuyTokensMessages(msgId: string) { + const pollingOptions = { + maxAttempts: 50, + pollingIntervalMs: 3_000, + initialBackoffMs: 7_000 + } + const { maxAttempts, pollingIntervalMs, initialBackoffMs } = pollingOptions + + console.log('Polling for transaction...', { msgId }) + await waitFor(initialBackoffMs) + + let attempts = 0 + while (attempts < maxAttempts) { + let transaction + attempts++ + + try { + const response = await arweave.api.post('/graphql', { + query: + 'query ($messageId: String!, $limit: Int!, $sortOrder: SortOrder!, $cursor: String) {\n transactions(\n sort: $sortOrder\n first: $limit\n after: $cursor\n tags: [{name: \n"Pushed-For", values: [$messageId]}]\n ) {\n count\n ...MessageFields\n }\n}\nfragment MessageFields on TransactionConnection {\n edges {\n cursor\n node {\n id\n ingested_at\n recipient\n block {\n timestamp\n height\n }\n tags {\n name\n value\n }\n data {\n size\n }\n owner {\n address\n }\n }\n }\n}', + variables: { + messageId: msgId, + limit: 100, + sortOrder: 'INGESTED_AT_ASC', + cursor: '' + } + }) + + transaction = response?.data?.data?.transactions + console.log({ transaction }) + } catch (err) { + // Continue retries when request errors + console.log('Failed to poll for transaction...', { err }) + } + + if (transaction) { + const messages = transaction.edges.map((edge: any) => edge.node) + + for (const msg of messages) { + const buyTokensErrorMessage = msg.tags.find( + (tag: any) => tag.name === 'Action' && tag.value === BUY_TOKENS_ERROR_ACTION + ) + + if (buyTokensErrorMessage) { + const errMessage = msg.tags.find((tag: any) => tag.name === 'Error')?.value + + return { success: false, message: errMessage } + } + + const buyTokensResponseMessage = msg.tags.find( + (tag: any) => tag.name === 'Action' && tag.value === BUY_TOKENS_RESPONSE_ACTION + ) + + if (buyTokensResponseMessage) { + const data = msg.tags.find((tag: any) => tag.name === 'Data')?.value + return { success: true, message: data } + } + } + } + console.log('Transaction not found...', { + msgId, + attempts, + maxAttempts, + pollingIntervalMs + }) + await waitFor(pollingIntervalMs) + } + + throw new Error('Transaction not found after polling, transaction id: ' + msgId) +} + +const SELL_TOKENS_RESPONSE_ACTION = 'Sell-Tokens-Response' +const SELL_TOKENS_ERROR_ACTION = 'Sell-Tokens-Error' + +export async function pollSellTokensMessages(msgId: string) { + const pollingOptions = { + maxAttempts: 50, + pollingIntervalMs: 3_000, + initialBackoffMs: 7_000 + } + const { maxAttempts, pollingIntervalMs, initialBackoffMs } = pollingOptions + + console.log('Polling for transaction...', { msgId }) + await waitFor(initialBackoffMs) + + let attempts = 0 + while (attempts < maxAttempts) { + let transaction + attempts++ + + try { + const response = await arweave.api.post('/graphql', { + query: + 'query ($messageId: String!, $limit: Int!, $sortOrder: SortOrder!, $cursor: String) {\n transactions(\n sort: $sortOrder\n first: $limit\n after: $cursor\n tags: [{name: \n"Pushed-For", values: [$messageId]}]\n ) {\n count\n ...MessageFields\n }\n}\nfragment MessageFields on TransactionConnection {\n edges {\n cursor\n node {\n id\n ingested_at\n recipient\n block {\n timestamp\n height\n }\n tags {\n name\n value\n }\n data {\n size\n }\n owner {\n address\n }\n }\n }\n}', + variables: { + messageId: msgId, + limit: 100, + sortOrder: 'INGESTED_AT_ASC', + cursor: '' + } + }) + + transaction = response?.data?.data?.transactions + console.log({ transaction }) + } catch (err) { + // Continue retries when request errors + console.log('Failed to poll for transaction...', { err }) + } + + if (transaction) { + const messages = transaction.edges.map((edge: any) => edge.node) + + for (const msg of messages) { + const sellTokensErrorMessage = msg.tags.find( + (tag: any) => tag.name === 'Action' && tag.value === SELL_TOKENS_ERROR_ACTION + ) + + if (sellTokensErrorMessage) { + const errMessage = msg.tags.find((tag: any) => tag.name === 'Error')?.value + + return { success: false, message: errMessage } + } + + const sellTokensResponseMessage = msg.tags.find( + (tag: any) => tag.name === 'Action' && tag.value === SELL_TOKENS_RESPONSE_ACTION + ) + + if (sellTokensResponseMessage) { + const data = msg.tags.find((tag: any) => tag.name === 'Data')?.value + return { success: true, message: data } + } + } + } + console.log('Transaction not found...', { + msgId, + attempts, + maxAttempts, + pollingIntervalMs + }) + await waitFor(pollingIntervalMs) + } + + throw new Error('Transaction not found after polling, transaction id: ' + msgId) +} + +const DEPOSIT_TO_LIQUIDITY_POOL_RESPONSE_ACTION = 'Deposit-To-Liquidity-Pool-Response' +const DEPOSIT_TO_LIQUIDITY_POOL_ERROR_ACTION = 'Deposit-To-Liquidity-Pool-Error' + +export async function pollDepositToLiquidityPoolMessages(msgId: string) { + const pollingOptions = { + maxAttempts: 50, + pollingIntervalMs: 3_000, + initialBackoffMs: 7_000 + } + const { maxAttempts, pollingIntervalMs, initialBackoffMs } = pollingOptions + + console.log('Polling for transaction...', { msgId }) + await waitFor(initialBackoffMs) + + let attempts = 0 + while (attempts < maxAttempts) { + let transaction + attempts++ + + try { + const response = await arweave.api.post('/graphql', { + query: + 'query ($messageId: String!, $limit: Int!, $sortOrder: SortOrder!, $cursor: String) {\n transactions(\n sort: $sortOrder\n first: $limit\n after: $cursor\n tags: [{name: \n"Pushed-For", values: [$messageId]}]\n ) {\n count\n ...MessageFields\n }\n}\nfragment MessageFields on TransactionConnection {\n edges {\n cursor\n node {\n id\n ingested_at\n recipient\n block {\n timestamp\n height\n }\n tags {\n name\n value\n }\n data {\n size\n }\n owner {\n address\n }\n }\n }\n}', + variables: { + messageId: msgId, + limit: 100, + sortOrder: 'INGESTED_AT_ASC', + cursor: '' + } + }) + + transaction = response?.data?.data?.transactions + console.log({ transaction }) + } catch (err) { + // Continue retries when request errors + console.log('Failed to poll for transaction...', { err }) + } + + if (transaction) { + const messages = transaction.edges.map((edge: any) => edge.node) + + for (const msg of messages) { + const depositToLiquidityPoolErrorMessage = msg.tags.find( + (tag: any) => tag.name === 'Action' && tag.value === DEPOSIT_TO_LIQUIDITY_POOL_ERROR_ACTION + ) + + if (depositToLiquidityPoolErrorMessage) { + const errMessage = msg.tags.find((tag: any) => tag.name === 'Error')?.value + + return { success: false, message: errMessage } + } + + const depositToLiquidityPoolResponseMessage = msg.tags.find( + (tag: any) => tag.name === 'Action' && tag.value === DEPOSIT_TO_LIQUIDITY_POOL_RESPONSE_ACTION + ) + + if (depositToLiquidityPoolResponseMessage) { + const data = msg.tags.find((tag: any) => tag.name === 'Data')?.value + return { success: true, message: data } + } + } + } + console.log('Transaction not found...', { + msgId, + attempts, + maxAttempts, + pollingIntervalMs + }) + await waitFor(pollingIntervalMs) + } + + throw new Error('Transaction not found after polling, transaction id: ' + msgId) +} + +export async function getBuySellTransactionsOfCurve(curveId: string) { + const queryObject = prepareBuySellTransactionsQueryObject() + const edges = await goldsky.all(queryObject.query, { + sortOrder: 'HEIGHT_ASC', + action: [BUY_TOKENS_RESPONSE_ACTION, SELL_TOKENS_RESPONSE_ACTION], + curveId + }) + + return edges +} + +const prepareBuySellTransactionsQueryObject = () => { + return { + query: ` + query($cursor: String, $sortOrder: SortOrder!, $action: [String!], $curveId: [String!]) { + transactions( + first: 100 + after: $cursor + sort: $sortOrder + tags: [ + { name: "Action", values: $action } + { name: "From-Process", values: $curveId } + ] + ) { + pageInfo { + hasNextPage + } + edges { + node { + ...TransactionCommon + } + } + } + } + fragment TransactionCommon on Transaction { + id + ingested_at + recipient + block { + height + timestamp + } + tags { + name + value + } + } + ` + } +} + +export async function depositToLPFromBondingCurve(poolId: string, bondingCurveId: string) { + const args = { + tags: getTags({ + Action: 'Deposit-To-Liquidity-Pool', + 'Pool-Id': poolId + }), + pid: bondingCurveId + } + + const msgId = await sendMessage(args) + await pollForTxBeingAvailable({ txId: msgId }) + + const poolStatus = await pollDepositToLiquidityPoolMessages(msgId) + + return poolStatus +} diff --git a/src/lib/bonding-curve/sell.ts b/src/lib/bonding-curve/sell.ts new file mode 100644 index 00000000..d1d4918c --- /dev/null +++ b/src/lib/bonding-curve/sell.ts @@ -0,0 +1,55 @@ +import { getTags } from '@/helpers/getTags' + +import { sendMessage } from '../contract' +import { pollForTxBeingAvailable } from '../decentralize' +import { pollSellTokensMessages } from '.' +import { N_PLUS_1 } from './constants' +import { getTokenCurrentSupply } from './helpers' + +export async function calculateTokensSellCost( + repoTokenPID: string, + tokensToSell: number, + fundingGoal: number, + supplyToSell: number +): Promise { + // Fetch the current supply + const currentSupply = await getTokenCurrentSupply(repoTokenPID) + const s1 = parseInt(currentSupply) + + // Correct the calculation by subtracting tokensToSell + const s2 = s1 - tokensToSell + + if (s2 < 0) { + throw new Error('Supply cannot go below zero') + } + + const S_exp = Math.pow(supplyToSell, N_PLUS_1) + + // Calculate s1_exp and s2_exp, scaling if necessary + const s1_exp = Math.pow(s1, N_PLUS_1) + const s2_exp = Math.pow(s2, N_PLUS_1) + + // Calculate the proceeds + const numerator = fundingGoal * (s1_exp - s2_exp) + const proceeds = numerator / S_exp + + return proceeds +} + +export async function sellTokens(bondingCurvePID: string, tokensToSell: string) { + console.log('sellTokens', bondingCurvePID, tokensToSell) + + const args = { + tags: getTags({ + Action: 'Sell-Tokens', + 'Token-Quantity': tokensToSell + }), + pid: bondingCurvePID + } + + const msgId = await sendMessage(args) + await pollForTxBeingAvailable({ txId: msgId }) + + const { success, message } = await pollSellTokensMessages(msgId) + return { success, message } +} diff --git a/src/lib/contract/index.ts b/src/lib/contract/index.ts index 72b1fd47..2d4b9237 100644 --- a/src/lib/contract/index.ts +++ b/src/lib/contract/index.ts @@ -12,6 +12,7 @@ export type SendMessageArgs = { name: string value: string }[] + pid?: string anchor?: string } @@ -28,10 +29,10 @@ export async function getRepo(id: string) { return repo } -export async function sendMessage({ tags, data }: SendMessageArgs) { +export async function sendMessage({ tags, data, pid }: SendMessageArgs) { const signer = await getSigner({ injectedSigner: false }) const args = { - process: AOS_PROCESS_ID, + process: pid || AOS_PROCESS_ID, tags, signer: createDataItemSigner(signer) } as any @@ -42,7 +43,7 @@ export async function sendMessage({ tags, data }: SendMessageArgs) { const { Output } = await result({ message: messageId, - process: AOS_PROCESS_ID + process: pid || AOS_PROCESS_ID }) if (Output?.data?.output) { diff --git a/src/lib/decentralize/index.ts b/src/lib/decentralize/index.ts new file mode 100644 index 00000000..30aa5b16 --- /dev/null +++ b/src/lib/decentralize/index.ts @@ -0,0 +1,469 @@ +import { createDataItemSigner, dryrun, result, spawn } from '@permaweb/aoconnect' +import Arweave from 'arweave' +import { Tag } from 'arweave/web/lib/transaction' + +import { getTags } from '@/helpers/getTags' +import { waitFor } from '@/helpers/waitFor' +import { getSigner } from '@/helpers/wallet/getSigner' +import { createCurveBondedTokenLua } from '@/pages/repository/helpers/createTokenLua' +import { BondingCurve, RepoToken } from '@/types/repository' + +import { sendMessage } from '../contract' + +const arweave = new Arweave({ + host: 'arweave-search.goldsky.com', + port: 443, + protocol: 'https' +}) + +export async function decentralizeRepo(repoId: string) { + await sendMessage({ + tags: getTags({ + Action: 'Decentralize-Repo', + Id: repoId + }) + }) +} + +export async function createRepoToken(token: RepoToken) { + try { + const pid = await spawnTokenProcess(token.tokenName) + await loadTokenProcess(token, pid) + + return pid + } catch (error) { + console.log({ error }) + return false + } +} + +export async function spawnTokenProcess(tokenName: string, processType?: string) { + const signer = await getSigner({ injectedSigner: false }) + const aosDetails = await getAosDetails() + const tags = [ + { name: 'App-Name', value: 'aos' }, + { name: 'Name', value: tokenName || 'Protocol.Land Repo Token' }, + { name: 'Process-Type', value: processType || 'token' }, + { name: 'aos-Version', value: aosDetails.version }, + { name: 'Authority', value: 'fcoN_xJeisVsPXA-trzVAuIiqO3ydLQxM-L4XbrQKzY' } + ] as Tag[] + + const pid = await spawn({ + module: aosDetails.module, + tags, + scheduler: aosDetails.scheduler, + data: '1984', + signer: createDataItemSigner(signer) + }) + + await pollForTxBeingAvailable({ txId: pid }) + + return pid +} + +export async function spawnBondingCurveProcess(tokenName: string, processType?: string) { + const signer = await getSigner({ injectedSigner: false }) + const aosDetails = await getAosDetails() + const tags = [ + { name: 'App-Name', value: 'aos' }, + { name: 'Name', value: tokenName ? tokenName + ' Bonding Curve' : 'Protocol.Land Repo Bonding Curve' }, + { name: 'Process-Type', value: processType || 'bonding-curve' }, + { name: 'aos-Version', value: aosDetails.version }, + { name: 'Authority', value: 'fcoN_xJeisVsPXA-trzVAuIiqO3ydLQxM-L4XbrQKzY' } + ] as Tag[] + + const pid = await spawn({ + module: aosDetails.module, + tags, + scheduler: aosDetails.scheduler, + data: '1984', + signer: createDataItemSigner(signer) + }) + + await pollForTxBeingAvailable({ txId: pid }) + + const sourceCodeFetchRes = await fetch('/contracts/curve-bonded-token-manager.lua') + const sourceCode = await sourceCodeFetchRes.text() + + const args = { + tags: getTags({ + Action: 'Eval' + }), + data: sourceCode, + pid: pid + } + + const msgId = await sendMessage(args) + await pollForTxBeingAvailable({ txId: msgId }) + + return pid +} + +export async function loadTokenProcess(token: RepoToken, pid: string) { + const contractSrc = createCurveBondedTokenLua(token, pid) + + const args = { + tags: getTags({ + Action: 'Eval' + }), + data: contractSrc, + pid: token.processId! + } + + const msgId = await sendMessage(args) + await pollForTxBeingAvailable({ txId: msgId }) + + const { Messages } = await dryrun({ + process: token.processId!, + tags: getTags({ + Action: 'Info' + }) + }) + + if (!Messages[0]) { + throw new Error('Token Loading Failed') + } + + const msg = Messages[0] + const ticker = msg.Tags.find((tag: any) => tag.name === 'Ticker')?.value + + if (!ticker) { + throw new Error('Token Loading Failed') + } + + return true +} + +export async function initializeBondingCurve(token: RepoToken, bondingCurve: BondingCurve) { + const args = { + tags: getTags({ + Action: 'Initialize-Bonding-Curve' + }), + data: JSON.stringify({ + repoToken: token, + reserveToken: bondingCurve.reserveToken, + fundingGoal: bondingCurve.fundingGoal, + allocationForLP: '0', + allocationForCreator: '0', + maxSupply: token.totalSupply + }), + pid: bondingCurve.processId + } + + const msgId = await sendMessage(args) + await pollForTxBeingAvailable({ txId: msgId }) + + const { Messages } = await result({ message: msgId, process: bondingCurve.processId! }) + + if (!Messages[0]) { + throw new Error('Bonding Curve Initialization Failed') + } + + const msg = Messages[0] + const error = msg.Tags.find((tag: any) => tag.name === 'Error')?.value + + if (error) { + throw new Error('Bonding Curve Initialization Failed') + } + + return true +} + +async function getAosDetails() { + const defaultDetails = { + version: '1.10.22', + module: 'SBNb1qPQ1TDwpD_mboxm2YllmMLXpWw4U8P9Ff8W9vk', + scheduler: '_GQ33BkPtZrqxA84vM8Zk-N2aO0toNNu_C-l-rawrBA' + } + + try { + const response = await fetch('https://raw.githubusercontent.com/permaweb/aos/main/package.json') + const pkg = (await response.json()) as { + version: string + aos: { module: string } + } + const details = { + version: pkg?.version || defaultDetails.version, + module: pkg?.aos?.module || defaultDetails.module, + scheduler: defaultDetails.scheduler + } + return details + } catch { + return defaultDetails + } +} + +export async function pollForTxBeingAvailable({ txId }: { txId: string }): Promise { + const pollingOptions = { + maxAttempts: 10, + pollingIntervalMs: 3_000, + initialBackoffMs: 7_000 + } + const { maxAttempts, pollingIntervalMs, initialBackoffMs } = pollingOptions + + console.log('Polling for transaction...', { txId }) + await waitFor(initialBackoffMs) + + let attempts = 0 + while (attempts < maxAttempts) { + let transaction + attempts++ + + try { + const response = await arweave.api.post('/graphql', { + query: ` + query { + transaction(id: "${txId}") { + recipient + owner { + address + } + quantity { + winston + } + } + } + ` + }) + + transaction = response?.data?.data?.transaction + } catch (err) { + // Continue retries when request errors + console.log('Failed to poll for transaction...', { err }) + } + + if (transaction) { + return + } + console.log('Transaction not found...', { + txId, + attempts, + maxAttempts, + pollingIntervalMs + }) + await waitFor(pollingIntervalMs) + } + + throw new Error('Transaction not found after polling, transaction id: ' + txId) +} + +const POOL_CONFIRM_ACTION = 'Add-Pool-Confirmation' +const POOL_ERROR_ACTION = 'Add-Pool-Error' + +export async function pollLiquidityPoolMessages(msgId: string) { + const pollingOptions = { + maxAttempts: 50, + pollingIntervalMs: 3_000, + initialBackoffMs: 7_000 + } + const { maxAttempts, pollingIntervalMs, initialBackoffMs } = pollingOptions + + console.log('Polling for transaction...', { msgId }) + await waitFor(initialBackoffMs) + + let attempts = 0 + while (attempts < maxAttempts) { + let transaction + attempts++ + + try { + const response = await arweave.api.post('/graphql', { + query: + 'query ($messageId: String!, $limit: Int!, $sortOrder: SortOrder!, $cursor: String) {\n transactions(\n sort: $sortOrder\n first: $limit\n after: $cursor\n tags: [{name: \n"Pushed-For", values: [$messageId]}]\n ) {\n count\n ...MessageFields\n }\n}\nfragment MessageFields on TransactionConnection {\n edges {\n cursor\n node {\n id\n ingested_at\n recipient\n block {\n timestamp\n height\n }\n tags {\n name\n value\n }\n data {\n size\n }\n owner {\n address\n }\n }\n }\n}', + variables: { + messageId: msgId, + limit: 100, + sortOrder: 'INGESTED_AT_ASC', + cursor: '' + } + }) + + transaction = response?.data?.data?.transactions + console.log({ transaction }) + } catch (err) { + // Continue retries when request errors + console.log('Failed to poll for transaction...', { err }) + } + + if (transaction) { + const messages = transaction.edges.map((edge: any) => edge.node) + + for (const msg of messages) { + const poolErrorMessage = msg.tags.find((tag: any) => tag.name === 'Action' && tag.value === POOL_ERROR_ACTION) + const poolStatus = msg.tags.find((tag: any) => tag.name === 'Status')?.value + + if (poolErrorMessage) { + const errMessage = msg.tags.find((tag: any) => tag.name === 'Error')?.value + console.log({ poolId: null, poolStatus, message: errMessage }) + return { poolId: null, poolStatus, message: errMessage } + } + + const poolConfirmationMessage = msg.tags.find( + (tag: any) => tag.name === 'Action' && tag.value === POOL_CONFIRM_ACTION + ) + + if (poolConfirmationMessage) { + const poolId = msg.tags.find((tag: any) => tag.name === 'Pool-Id')?.value + console.log({ poolId, poolStatus }) + return { poolId, poolStatus } + } + } + } + console.log('Transaction not found...', { + msgId, + attempts, + maxAttempts, + pollingIntervalMs + }) + await waitFor(pollingIntervalMs) + } + + throw new Error('Transaction not found after polling, transaction id: ' + msgId) +} + +const PROVIDE_CONFIRM_ACTION = 'Provide-Confirmation' +const PROVIDE_ERROR_ACTION = 'Provide-Error' +export async function pollLiquidityProvideMessages(msgId: string) { + const pollingOptions = { + maxAttempts: 50, + pollingIntervalMs: 3_000, + initialBackoffMs: 7_000 + } + const { maxAttempts, pollingIntervalMs, initialBackoffMs } = pollingOptions + + console.log('Polling for transaction...', { msgId }) + await waitFor(initialBackoffMs) + + let attempts = 0 + while (attempts < maxAttempts) { + let transaction + attempts++ + + try { + const response = await arweave.api.post('/graphql', { + query: + 'query ($messageId: String!, $limit: Int!, $sortOrder: SortOrder!, $cursor: String) {\n transactions(\n sort: $sortOrder\n first: $limit\n after: $cursor\n tags: [{name: \n"Pushed-For", values: [$messageId]}] \n ) {\n count\n ...MessageFields\n }\n}\nfragment MessageFields on TransactionConnection {\n edges {\n cursor\n node {\n id\n ingested_at\n recipient\n block {\n timestamp\n height\n }\n tags {\n name\n value\n }\n data {\n size\n }\n owner {\n address\n }\n }\n }\n}', + variables: { + messageId: msgId, + limit: 100, + sortOrder: 'INGESTED_AT_ASC', + cursor: '' + } + }) + + transaction = response?.data?.data?.transactions + console.log({ transaction }) + } catch (err) { + // Continue retries when request errors + console.log('Failed to poll for transaction...', { err }) + } + + if (transaction) { + const messages = transaction.edges.map((edge: any) => edge.node) + + for (const msg of messages) { + const provideErrorMessage = msg.tags.find( + (tag: any) => tag.name === 'Action' && tag.value === PROVIDE_ERROR_ACTION + ) + + if (provideErrorMessage) { + const errMessage = msg.tags.find((tag: any) => tag.name === 'Error')?.value + return { status: 'ERROR', message: errMessage || 'Provide failed.' } + } + + const provideConfirmationMessage = msg.tags.find( + (tag: any) => tag.name === 'Action' && tag.value === PROVIDE_CONFIRM_ACTION + ) + + if (provideConfirmationMessage) { + return { status: 'OK' } + } + } + } + console.log('Transaction not found...', { + msgId, + attempts, + maxAttempts, + pollingIntervalMs + }) + await waitFor(pollingIntervalMs) + } + + throw new Error('Transaction not found after polling, transaction id: ' + msgId) +} + +export async function fetchTokenBalance(tokenId: string, address: string) { + const { Messages } = await dryrun({ + process: tokenId, + owner: address, + tags: [ + { name: 'Action', value: 'Balance' }, + { name: 'Target', value: address } + ] + }) + + if (!Messages || !Messages.length) { + console.log('No message found', { tokenId, address }) + + return '0' + } + const balanceTagValue = Messages[0].Tags.find((tag: any) => tag.name === 'Balance')?.value + const balance = Messages[0].Data ? Messages[0].Data : balanceTagValue || '0' + + return balance +} + +export async function fetchTokenBalances(tokenId: string) { + const { Messages } = await dryrun({ + process: tokenId, + tags: [{ name: 'Action', value: 'Balances' }] + }) + + if (!Messages || !Messages.length) { + console.log('No message found', { tokenId }) + + return '0' + } + const balances = JSON.parse(Messages[0].Data) + + return balances +} + +export async function fetchTokenInfo(tokenId: string) { + const { Messages } = await dryrun({ + process: tokenId, + tags: [{ name: 'Action', value: 'Info' }] + }) + + if (!Messages || !Messages.length) { + console.log('No message found', { tokenId }) + + return null + } + + const tokenData = decodeTokenDetailsFromMessage(Messages[0]) + + return { + tokenName: tokenData.name, + tokenTicker: tokenData.ticker, + tokenImage: tokenData.logo, + processId: tokenId, + denomination: tokenData.denomination.toString() + } +} + +function decodeTokenDetailsFromMessage(msg: any) { + const tags = msg.Tags as Tag[] + const tagsToPick = ['Name', 'Ticker', 'Denomination', 'Logo'] + + const tokenData = tags.reduce((acc, curr) => { + if (tagsToPick.includes(curr.name)) { + const key = curr.name.toLowerCase() + acc[key] = curr.value + } + + return acc + }, {} as any) + + return tokenData +} diff --git a/src/lib/dragondeploy/index.ts b/src/lib/dragondeploy/index.ts index d3ed8e24..388c8fd1 100644 --- a/src/lib/dragondeploy/index.ts +++ b/src/lib/dragondeploy/index.ts @@ -1,7 +1,7 @@ import ArDB from 'ardb' import Arweave from 'arweave' import { Tag } from 'arweave/web/lib/transaction' -import git, { WORKDIR } from 'isomorphic-git' +import git, { WORKDIR } from '@protocol.land/isomorphic-git' import mime from 'mime' import type { Dispatch, SetStateAction } from 'react' diff --git a/src/lib/git/branch.ts b/src/lib/git/branch.ts index 120415ff..066c573c 100644 --- a/src/lib/git/branch.ts +++ b/src/lib/git/branch.ts @@ -1,4 +1,4 @@ -import git from 'isomorphic-git' +import git from '@protocol.land/isomorphic-git' import { withAsync } from '@/helpers/withAsync' @@ -47,7 +47,8 @@ export async function checkoutBranch({ fs, dir, name }: CommonBranchOptions & { dir, ref: name, force: true, - track: false + track: false, + noUpdateHead: true }) ) } diff --git a/src/lib/git/commit.ts b/src/lib/git/commit.ts index 57122303..7b764055 100644 --- a/src/lib/git/commit.ts +++ b/src/lib/git/commit.ts @@ -1,4 +1,4 @@ -import git from 'isomorphic-git' +import git from '@protocol.land/isomorphic-git' import { FileWithPath } from 'react-dropzone' import { toArrayBuffer } from '@/helpers/toArrayBuffer' diff --git a/src/lib/git/helpers/fsWithName.ts b/src/lib/git/helpers/fsWithName.ts index 81401fd0..82cf619b 100644 --- a/src/lib/git/helpers/fsWithName.ts +++ b/src/lib/git/helpers/fsWithName.ts @@ -1,7 +1,15 @@ -import LightningFS from '@isomorphic-git/lightning-fs' +import arfsSingletonMap from '@/lib/arfs/arfsSingletonMap' export function fsWithName(name: string) { - return new LightningFS(name) + const arfsSingleton = arfsSingletonMap.getArFSSingleton(name) + + if (!arfsSingleton) throw new Error('ArFS uninitialized.') + + const bifrost = arfsSingleton.getBifrostInstance() + + if (!bifrost) throw new Error('Bifrost uninitialized.') + + return bifrost.fs } -export type FSType = ReturnType \ No newline at end of file +export type FSType = ReturnType diff --git a/src/lib/git/helpers/oid.ts b/src/lib/git/helpers/oid.ts index 3df61daf..a94a2f8c 100644 --- a/src/lib/git/helpers/oid.ts +++ b/src/lib/git/helpers/oid.ts @@ -1,4 +1,4 @@ -import git, { TreeEntry } from 'isomorphic-git' +import git, { TreeEntry } from '@protocol.land/isomorphic-git' import { FSType } from './fsWithName' diff --git a/src/lib/git/helpers/zipUtils.ts b/src/lib/git/helpers/zipUtils.ts index 30b68e30..01047cf7 100644 --- a/src/lib/git/helpers/zipUtils.ts +++ b/src/lib/git/helpers/zipUtils.ts @@ -1,6 +1,7 @@ import JSZip from 'jszip' import { waitFor } from '@/helpers/waitFor' +import { withAsync } from '@/helpers/withAsync' import { FSType } from './fsWithName' @@ -33,6 +34,41 @@ export async function unpackGitRepo({ fs, blob }: UnpackGitRepoOptions) { return true } +export async function copyFilesToTargetRepo( + sourceDirPath: string, + sourceFS: FSType, + targetFS: FSType, + targetDir: string +) { + // ensure targetFS is initialized with dir + const { error } = await withAsync(() => targetFS.promises.readdir(targetDir)) + if (error) { + await targetFS.promises.mkdir(targetDir) + } + + const dirItems = await sourceFS.promises.readdir(sourceDirPath) + + dirItems.forEach(async (item) => { + const srcPath = `${sourceDirPath}/${item}` + const destPath = `${targetDir}/${item}` + + const stats = await sourceFS.promises.stat(srcPath) + + if (stats.isDirectory()) { + try { + await targetFS.promises.mkdir(destPath) + } catch (error) { + // ignore + } + + await copyFilesToTargetRepo(srcPath, sourceFS, targetFS, destPath) + } else { + const fileContent = await sourceFS.promises.readFile(srcPath) + await targetFS.promises.writeFile(destPath, fileContent) + } + }) +} + async function addFilesToZip(zip: JSZip, path: string, fs: FSType) { const dirItems = await fs.promises.readdir(path) diff --git a/src/lib/git/pull-request.ts b/src/lib/git/pull-request.ts index 958ff383..c9d6ee5e 100644 --- a/src/lib/git/pull-request.ts +++ b/src/lib/git/pull-request.ts @@ -1,4 +1,4 @@ -import git, { Errors } from 'isomorphic-git' +import git, { Errors } from '@protocol.land/isomorphic-git' import { getTags } from '@/helpers/getTags' import { trackGoogleAnalyticsEvent } from '@/helpers/google-analytics' diff --git a/src/lib/git/repo.ts b/src/lib/git/repo.ts index ba5f2faf..c99cba3d 100644 --- a/src/lib/git/repo.ts +++ b/src/lib/git/repo.ts @@ -1,8 +1,7 @@ +import git from '@protocol.land/isomorphic-git' import Arweave from 'arweave' import { Tag } from 'arweave/web/lib/transaction' import Dexie from 'dexie' -import git from 'isomorphic-git' -import { v4 as uuidv4 } from 'uuid' import { getTags } from '@/helpers/getTags' import { toArrayBuffer } from '@/helpers/toArrayBuffer' @@ -33,43 +32,21 @@ const arweave = new Arweave({ protocol: 'https' }) -export async function postNewRepo({ id, title, description, file, owner, visibility }: any) { - const userSigner = await getSigner() - - const data = (await toArrayBuffer(file)) as ArrayBuffer - - const inputTags = [ - { name: 'App-Name', value: 'Protocol.Land' }, - { name: 'Content-Type', value: file.type }, - { name: 'Creator', value: owner }, - { name: 'Title', value: title }, - { name: 'Description', value: description }, - { name: 'Repo-Id', value: id }, - { name: 'Type', value: 'repo-create' }, - { name: 'Visibility', value: visibility } - ] as Tag[] - - await waitFor(500) - - const dataTxResponse = await signAndSendTx(data, inputTags, userSigner, true) - - if (!dataTxResponse) { - throw new Error('Failed to post Git repository') - } - +export async function postNewRepo({ id, dataTxId, title, description, tokenProcessId }: any) { await sendMessage({ tags: getTags({ Action: 'Initialize-Repo', Id: id, Name: title, Description: description, - 'Data-TxId': dataTxResponse, - Visibility: visibility, - 'Private-State-TxId': '' + 'Data-TxId': dataTxId, + Visibility: 'public', + 'Private-State-TxId': '', + 'Token-Process-Id': tokenProcessId }) }) - return { txResponse: dataTxResponse } + return { txResponse: dataTxId } } export async function updateGithubSync({ id, currentGithubSync, githubSync }: any) { @@ -147,12 +124,10 @@ export async function updateGithubSync({ id, currentGithubSync, githubSync }: an } export async function createNewFork(data: ForkRepositoryOptions) { - const uuid = uuidv4() - await sendMessage({ tags: getTags({ Action: 'Fork-Repo', - Id: uuid, + Id: data.id, Name: data.name, Description: data.description, 'Data-TxId': data.dataTxId, @@ -160,7 +135,7 @@ export async function createNewFork(data: ForkRepositoryOptions) { }) }) - return uuid + return data.id } export async function postUpdatedRepo({ fs, dir, owner, id }: PostUpdatedRepoOptions) { @@ -325,9 +300,7 @@ export async function createNewRepo(title: string, fs: FSType, owner: string, id await waitFor(1000) - const repoBlob = await packGitRepo({ fs, dir }) - - return { repoBlob, commit: sha } + return { commit: sha } } catch (error) { console.error('failed to create repo') } diff --git a/src/lib/queue/BaseQueue.ts b/src/lib/queue/BaseQueue.ts new file mode 100644 index 00000000..823afd0c --- /dev/null +++ b/src/lib/queue/BaseQueue.ts @@ -0,0 +1,52 @@ +import { QueueObserver } from './QueueObserver' + +export class BaseQueue { + public list: Array<{ token: string; payload: T }> = [] + protected addedList: Record = {} + private observers: QueueObserver[] = [] + + constructor() {} + + public enqueue(token: string, payload: T) { + if (!this.isInList(token)) { + this.list.unshift({ token, payload }) + this.addedList[token] = token + } + } + + public dequeue(token?: string) { + // you can pass token for removing specific item + if (token) { + const itemShouldRemove = this.list.find((item) => item.token === token) + this.list = this.list.filter((item) => item.token !== token) + + delete this.addedList[token] + + return itemShouldRemove + } else { + const item = this.list.pop() + + if (item) delete this.addedList[item?.token] + + return item + } + } + + public isInList(token: string) { + return token in this.addedList + } + + public addObserver(observer: QueueObserver) { + this.observers.push(observer) + } + + public notifyObservers(item: { token: string; payload: T }) { + for (const observer of this.observers) { + observer.update(item) + } + } + + public getList() { + return this.list + } +} diff --git a/src/lib/queue/PendingQueue.ts b/src/lib/queue/PendingQueue.ts new file mode 100644 index 00000000..09c1973f --- /dev/null +++ b/src/lib/queue/PendingQueue.ts @@ -0,0 +1,14 @@ +import Transaction from 'arweave/web/lib/transaction' +import { DataItem } from 'warp-arbundles' + +import { BaseQueue } from './BaseQueue' +import { QueueObserver } from './QueueObserver' + +export class PendingQueue extends BaseQueue {} + +export class PendingObserver implements QueueObserver { + update(item: { token: string; payload: Transaction | DataItem }) { + console.log({ item }, '<-- added to queue') + //check for progress queue length and move items from pending to progress + } +} diff --git a/src/lib/queue/QueueObserver.ts b/src/lib/queue/QueueObserver.ts new file mode 100644 index 00000000..5d53d30e --- /dev/null +++ b/src/lib/queue/QueueObserver.ts @@ -0,0 +1,3 @@ +export abstract class QueueObserver { + abstract update(item: { token: string; payload: T }): void +} diff --git a/src/lib/queue/TaskQueue.ts b/src/lib/queue/TaskQueue.ts new file mode 100644 index 00000000..202ec8d3 --- /dev/null +++ b/src/lib/queue/TaskQueue.ts @@ -0,0 +1,122 @@ +import Transaction from 'arweave/web/lib/transaction' +import axios from 'axios' +import { DataItem } from 'warp-arbundles' + +import { useGlobalStore } from '@/stores/globalStore' + +import { bundleAndSignData } from '../subsidize/utils' +import { PendingObserver, PendingQueue } from './PendingQueue' + +export const MAX_LENGTH_PROGRESS_QUEUE = 5 + +let instance: TaskQueueSingleton +let taskQueueStatus: 'Busy' | 'Idle' = 'Idle' +export class TaskQueueSingleton { + pendingQueue = new PendingQueue() + + constructor() { + if (instance) { + throw new Error('You can only create one instance!') + } + + this.pendingQueue.addObserver(new PendingObserver()) + // eslint-disable-next-line @typescript-eslint/no-this-alias + instance = this + } + + getInstance() { + return this + } + + getTaskQueueStatus() { + return taskQueueStatus + } + + setTaskQueueStatus(status: 'Busy' | 'Idle') { + taskQueueStatus = status + } + + sendToPending(token: string, tx: Transaction | DataItem) { + this.pendingQueue.enqueue(token, tx) + this.pendingQueue.notifyObservers({ token, payload: tx }) + } + + getPending() { + return this.pendingQueue.getList() + } + + async execute(driveId: string) { + if (taskQueueStatus === 'Busy') { + //toast message notify that new batch cant be run + return [] + } + + taskQueueStatus = 'Busy' + + const dataItems = [] + const ids = [] + let bundle: Awaited> | null = null + + try { + while (this.pendingQueue.getList().length > 0) { + const item = this.pendingQueue.dequeue() + + if (item) { + const { payload } = item + + dataItems.push(payload as DataItem) + ids.push(await payload.id) + } + } + + bundle = await bundleAndSignData(dataItems, () => console.log('error: unsigned data items.')) + } catch (error) { + taskQueueStatus = 'Idle' + + return [] + } + + const userAddress = useGlobalStore.getState().authState.address + if (!bundle || !userAddress) { + taskQueueStatus = 'Idle' + + return [] + } + + try { + const dataBinary = bundle.getRaw() + const res = ( + await axios.post( + 'https://bundle.saikranthi.dev/api/v1/postrepo', + { + txBundle: dataBinary, + platform: 'UI', + owner: userAddress!, + driveId + }, + { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json' + } + } + ) + ).data + + if (!res || !res.success) { + throw new Error('Failed to subsidize your transaction. Please try again.') + } + + return ids + } catch (error: any) { + taskQueueStatus = 'Idle' + + return [] + } finally { + taskQueueStatus = 'Idle' + } + } +} + +const taskQueueSingleton = Object.freeze(new TaskQueueSingleton()) +export default taskQueueSingleton diff --git a/src/lib/queue/helpers.ts b/src/lib/queue/helpers.ts new file mode 100644 index 00000000..ae9e6919 --- /dev/null +++ b/src/lib/queue/helpers.ts @@ -0,0 +1,22 @@ +import Transaction, { Tag } from 'arweave/web/lib/transaction' +import { DataItem } from 'warp-arbundles' + +import { createAndSignDataItem } from '@/helpers/wallet/createAndSignDataItem' +import { getSigner } from '@/helpers/wallet/getSigner' + +export async function createSignedQueuePayload(tx: Transaction | DataItem) { + const data = tx.data + let tags = tx.tags as Tag[] + + tags = tags.map((tag) => { + const name = tag.get('name', { decode: true, string: true }) + const value = tag.get('value', { decode: true, string: true }) + + return { name, value } + }) as Tag[] + + const signer = await getSigner() + const dataItem = await createAndSignDataItem(data, tags, signer) + + return dataItem +} diff --git a/src/main.tsx b/src/main.tsx index bc3b40fb..5dcbfff0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -22,7 +22,8 @@ ReactDOM.createRoot(document.getElementById('root')!).render( 'SIGNATURE', 'DISPATCH', 'DECRYPT', - 'ENCRYPT' + 'ENCRYPT', + 'ACCESS_TOKENS' ], ensurePermissions: true }} diff --git a/src/pages/home/components/NewRepoModal.tsx b/src/pages/home/components/NewRepoModal.tsx index 54a30d43..9d138364 100644 --- a/src/pages/home/components/NewRepoModal.tsx +++ b/src/pages/home/components/NewRepoModal.tsx @@ -6,7 +6,6 @@ import { useForm } from 'react-hook-form' import toast from 'react-hot-toast' import SVG from 'react-inlinesvg' import { useNavigate } from 'react-router-dom' -import { v4 as uuidv4 } from 'uuid' import * as yup from 'yup' import CloseCrossIcon from '@/assets/icons/close-cross.svg' @@ -14,8 +13,11 @@ import { Button } from '@/components/common/buttons' import CostEstimatesToolTip from '@/components/CostEstimatesToolTip' import { trackGoogleAnalyticsEvent } from '@/helpers/google-analytics' import { withAsync } from '@/helpers/withAsync' +import { getArFS } from '@/lib/arfs/getArFS' +import { getBifrost } from '@/lib/arfs/getBifrost' +import { spawnTokenProcess } from '@/lib/decentralize' import { createNewRepo, postNewRepo } from '@/lib/git' -import { fsWithName } from '@/lib/git/helpers/fsWithName' +import taskQueueSingleton from '@/lib/queue/TaskQueue' import { useGlobalStore } from '@/stores/globalStore' import { isRepositoryNameAvailable } from '@/stores/repository-core/actions/repoMeta' @@ -57,7 +59,7 @@ export default function NewRepoModal({ setIsOpen, isOpen }: NewRepoModalProps) { async function handleCreateBtnClick(data: yup.InferType) { setIsSubmitting(true) - const id = uuidv4() + const arfs = getArFS() const { title, description } = data const owner = authState.address || 'Protocol.Land user' @@ -70,28 +72,35 @@ export default function NewRepoModal({ setIsOpen, isOpen }: NewRepoModalProps) { } try { - const fs = fsWithName(id) - const createdRepo = await createNewRepo(title, fs, owner, id) + const drive = await arfs.drive.create(title) + const bifrost = getBifrost(drive, arfs) + const tokenProcessId = await spawnTokenProcess(title) + const createdRepo = await createNewRepo(title, bifrost.fs, owner, drive.driveId!) - if (createdRepo && createdRepo.commit && createdRepo.repoBlob) { - const { repoBlob } = createdRepo + const taskQueueItemsLength = taskQueueSingleton.getPending().length + if (createdRepo && createdRepo.commit && taskQueueItemsLength > 0) { + const uploadedToArFS = await taskQueueSingleton.execute(drive.driveId!) + + if (uploadedToArFS.length !== taskQueueItemsLength) { + throw new Error('Failed to upload.') + } const result = await postNewRepo({ - id, + id: drive.driveId!, title, description, - file: repoBlob, - owner: authState.address, - visibility + visibility, + dataTxId: drive.id!, + tokenProcessId }) if (result.txResponse) { trackGoogleAnalyticsEvent('Repository', 'Successfully created a repo', 'Create new repo', { - repo_id: id, + repo_id: drive.id!, repo_name: title }) - navigate(`/repository/${id}`) + navigate(`/repository/${drive.driveId}`) } } } catch (error) { diff --git a/src/pages/repository/Repository.tsx b/src/pages/repository/Repository.tsx index 3783b4ec..85d31231 100644 --- a/src/pages/repository/Repository.tsx +++ b/src/pages/repository/Repository.tsx @@ -115,7 +115,11 @@ export default function Repository() { )} {isReady && (
- + {rootTabConfig .filter((tab) => { diff --git a/src/pages/repository/components/ForkModal.tsx b/src/pages/repository/components/ForkModal.tsx index 12afd605..c7235aae 100644 --- a/src/pages/repository/components/ForkModal.tsx +++ b/src/pages/repository/components/ForkModal.tsx @@ -1,4 +1,4 @@ -import { Dialog, Transition } from '@headlessui/react' +import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react' import { yupResolver } from '@hookform/resolvers/yup' import clsx from 'clsx' import React, { Fragment } from 'react' @@ -11,7 +11,12 @@ import * as yup from 'yup' import CloseCrossIcon from '@/assets/icons/close-cross.svg' import { Button } from '@/components/common/buttons' import { withAsync } from '@/helpers/withAsync' +import arfsSingletonMap from '@/lib/arfs/arfsSingletonMap' +import { getArFS } from '@/lib/arfs/getArFS' +import { getBifrost } from '@/lib/arfs/getBifrost' import { createNewFork } from '@/lib/git' +import { copyFilesToTargetRepo } from '@/lib/git/helpers/zipUtils' +import taskQueueSingleton from '@/lib/queue/TaskQueue' import { useGlobalStore } from '@/stores/globalStore' import { getRepositoryMetaFromContract, isRepositoryNameAvailable } from '@/stores/repository-core/actions/repoMeta' import { Repo } from '@/types/repository' @@ -61,11 +66,13 @@ export default function ForkModal({ setIsOpen, isOpen, repo }: NewRepoModalProps const { response: fetchedRepo } = await withAsync(() => getRepositoryMetaFromContract(repoId)) return fetchedRepo && fetchedRepo.result && fetchedRepo.result.forks[connectedAddress!] } - + console.log(arfsSingletonMap.getAllArFSSingletons()) async function handleCreateFork(data: yup.InferType) { + const arfs = getArFS() setIsSubmitting(true) const payload = { + id: '', name: data.title, description: data.description ?? '', parent: repo.id, @@ -88,15 +95,36 @@ export default function ForkModal({ setIsOpen, isOpen, repo }: NewRepoModalProps return } - const { response, error } = await withAsync(() => createNewFork(payload)) + try { + const drive = await arfs.drive.create(payload.name) + const bifrost = getBifrost(drive, arfs) - if (error) { - toast.error('Failed to fork this repo.') - } + payload.id = drive.driveId + + const currentRepoArFS = arfsSingletonMap.getArFSSingleton(repo.id) + + if (!currentRepoArFS || !currentRepoArFS.bifrostInstance) { + throw new Error('Current repo ArFS not found') + } - if (response) { - setIsOpen(false) - navigate(`/repository/${response}`) + const currentRepoFS = currentRepoArFS.bifrostInstance.fs + + const forkRepoFS = bifrost.fs + await copyFilesToTargetRepo(`/${repo.id}`, currentRepoFS, forkRepoFS, `/${drive.driveId}`) + const taskQueueItemsLength = taskQueueSingleton.getPending().length + + const uploadedToArFS = await taskQueueSingleton.execute(drive.driveId) + if (uploadedToArFS.length !== taskQueueItemsLength) { + throw new Error('Failed to upload.') + } + const response = await createNewFork(payload) + + if (response) { + setIsOpen(false) + navigate(`/repository/${response}`) + } + } catch (error) { + toast.error('Failed to fork this repo.') } } @@ -106,7 +134,7 @@ export default function ForkModal({ setIsOpen, isOpen, repo }: NewRepoModalProps return ( -
- +
- - +
- + Create a new Fork - +
@@ -182,8 +210,8 @@ export default function ForkModal({ setIsOpen, isOpen, repo }: NewRepoModalProps Create
-
-
+ +
diff --git a/src/pages/repository/components/RepoHeader.tsx b/src/pages/repository/components/RepoHeader.tsx index e7131212..2b8f3bf0 100644 --- a/src/pages/repository/components/RepoHeader.tsx +++ b/src/pages/repository/components/RepoHeader.tsx @@ -12,12 +12,17 @@ import IconForkOutline from '@/assets/icons/fork-outline.svg' import IconStarOutline from '@/assets/icons/star-outline.svg' import { Button } from '@/components/common/buttons' import { trackGoogleAnalyticsPageView } from '@/helpers/google-analytics' +import { imgUrlFormatter } from '@/helpers/imgUrlFormatter' import { resolveUsernameOrShorten } from '@/helpers/resolveUsername' +// import { fetchTokenBalance } from '@/lib/decentralize' +import { useGlobalStore } from '@/stores/globalStore' import { Repo } from '@/types/repository' import useRepository from '../hooks/useRepository' import { useRepoHeaderStore } from '../store/repoHeader' import ActivityGraph from './ActivityGraph' +import TokenizeModal from './decentralize-modals/Tokenize-Modal' +import TradeModal from './decentralize-modals/Trade-Modal' import ForkModal from './ForkModal' import RepoHeaderLoading from './RepoHeaderLoading' @@ -29,13 +34,16 @@ type Props = { } export default function RepoHeader({ repo, isLoading, owner, parentRepo }: Props) { + const [isDecentralizationModalOpen, setIsDecentralizationModalOpen] = React.useState(false) + const [isDecentralized, setIsDecentralized] = React.useState(false) const [isForkModalOpen, setIsForkModalOpen] = React.useState(false) + const [isTradeModalOpen, setIsTradeModalOpen] = React.useState(false) const [showCloneDropdown, setShowCloneDropdown] = React.useState(false) const cloneRef = React.useRef(null) const location = useLocation() const navigate = useNavigate() const { downloadRepository } = useRepository(repo?.id, repo?.name) - + const [isRepoOwner] = useGlobalStore((state) => [state.repoCoreActions.isRepoOwner]) const [repoHeaderState] = useRepoHeaderStore((state) => [state.repoHeaderState]) React.useEffect(() => { @@ -47,6 +55,24 @@ export default function RepoHeader({ repo, isLoading, owner, parentRepo }: Props } }, [repo]) + React.useEffect(() => { + if (isLoading === true) { + setIsDecentralized(false) + } + }, [isLoading]) + + React.useEffect(() => { + if (repo && repo?.decentralized === true && !isLoading) { + setIsDecentralized(true) + } + }, [repo, isLoading]) + + React.useEffect(() => { + if (repo && repo?.decentralized && repo?.token?.processId) { + fetchAndSetTokenBal() + } + }, [repo]) + if (isLoading) { return } @@ -90,6 +116,25 @@ export default function RepoHeader({ repo, isLoading, owner, parentRepo }: Props navigate(`/repository/${parentRepo.id}`) } + async function fetchAndSetTokenBal() { + if (!repo || !repo.token || !repo.token.processId) return + + // setTokenBalLoading(true) + // try { + // const bal = await fetchTokenBalance(repo.token.processId, address!) + // setTokenBal(bal) + // } catch (error) { + // toast.error('Failed to fetch token balance.') + // } + // setTokenBalLoading(false) + } + + function handleTradeClick() { + if (!repo || !repo.token || !repo.token.processId) return + + setIsTradeModalOpen(true) + } + return (
@@ -98,12 +143,52 @@ export default function RepoHeader({ repo, isLoading, owner, parentRepo }: Props

SK

-
+
-

{repo.name}

+
+

{repo.name}

+ +
{repo.private ? 'Private' : 'Public'} + {isDecentralized && ( + + Tokenized + + )} + {isDecentralized && repo.token && repo.token.processId && ( +
+
+ + {/* {tokenBalLoading && } + {!tokenBalLoading && ( + + {BigNumber(tokenBal) + .dividedBy(BigNumber(10 ** +repo.token.denomination)) + .toString()}{' '} + {repo.token.tokenTicker} + + )} + */} +
+
+ )}

Transaction ID: {repo.dataTxId} @@ -135,17 +220,33 @@ export default function RepoHeader({ repo, isLoading, owner, parentRepo }: Props

{repoHeaderState.repoSize}

+
+ +

0

+

{repo.description}

-
- +
+ {!isDecentralized && ( +
+ Tokenize + +
+ )} +
+ + {isDecentralizationModalOpen && ( + setIsDecentralizationModalOpen(false)} + isOpen={isDecentralizationModalOpen} + /> + )} + {isDecentralized && isTradeModalOpen && repo.token && repo.token.processId && ( + setIsTradeModalOpen(false)} isOpen={isTradeModalOpen} /> + )}
) } diff --git a/src/pages/repository/components/decentralize-modals/Confirm.tsx b/src/pages/repository/components/decentralize-modals/Confirm.tsx new file mode 100644 index 00000000..dc1473c8 --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/Confirm.tsx @@ -0,0 +1,73 @@ +import { useNavigate, useParams } from 'react-router-dom' + +import { Button } from '@/components/common/buttons' +import { imgUrlFormatter } from '@/helpers/imgUrlFormatter' +import { BondingCurve, RepoToken } from '@/types/repository' + +export default function Confirm({ + bondingCurve, + token, + onClose, + onAction +}: { + bondingCurve: BondingCurve + token: RepoToken + onClose: () => void + onAction: () => void +}) { + const { id } = useParams() + const navigate = useNavigate() + + function handleGoToSettings() { + if (!id) return + onClose() + navigate(`/repository/${id}/settings/token`) + } + + return ( + <> +
+
+ +
+
+
+ +

{token.tokenName}

+
+
+ +

{token.tokenTicker}

+
+
+ +

{token.denomination}

+
+
+ +

{token.totalSupply}

+
+
+ +

{bondingCurve.fundingGoal}

+
+
+ +

+ {bondingCurve.reserveToken.tokenName} - ({bondingCurve.reserveToken.tokenTicker}) +

+
+
+
+ +
+ + +
+ + ) +} diff --git a/src/pages/repository/components/decentralize-modals/Decentralize-Error.tsx b/src/pages/repository/components/decentralize-modals/Decentralize-Error.tsx new file mode 100644 index 00000000..34e11778 --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/Decentralize-Error.tsx @@ -0,0 +1,87 @@ +import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react' +import { Fragment } from 'react' +import SVG from 'react-inlinesvg' + +import CloseCrossIcon from '@/assets/icons/close-cross.svg' +import { Button } from '@/components/common/buttons' + +import { ERROR_MESSAGE_TYPES, ErrorMessageTypes } from './config' + +type NewRepoModalProps = { + onClose: () => void + isOpen: boolean + errorType: ErrorMessageTypes + onActionClick?: () => void | Promise +} + +export default function DecentralizeError({ onClose, isOpen, errorType, onActionClick }: NewRepoModalProps) { + const { title, description, icon: Icon, actionText } = ERROR_MESSAGE_TYPES[errorType] + + function closeModal() { + onClose() + } + + return ( + + + +
+ + +
+
+ + +
+ + Decentralize Repo + + +
+
+
+ +
+

{title || 'Error'}

+

+ {description || 'An error occurred while trying to decentralize the repository.'} +

+
+
+
+ + {onActionClick && ( +
+ +
+ )} +
+
+
+
+
+
+ ) +} diff --git a/src/pages/repository/components/decentralize-modals/Decentralize-Success.tsx b/src/pages/repository/components/decentralize-modals/Decentralize-Success.tsx new file mode 100644 index 00000000..b2549808 --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/Decentralize-Success.tsx @@ -0,0 +1,77 @@ +import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react' +import { Fragment } from 'react' +import SVG from 'react-inlinesvg' + +import CloseCrossIcon from '@/assets/icons/close-cross.svg' +import { Button } from '@/components/common/buttons' +import { RepoToken } from '@/types/repository' + +type NewRepoModalProps = { + onClose: () => void + isOpen: boolean + token: RepoToken +} + +export default function DecentralizeSuccess({ onClose, isOpen, token }: NewRepoModalProps) { + function closeModal() { + onClose() + } + + return ( + + + +
+ + +
+
+ + +
+ + Repository Decentralized + + +
+
+
+ +

+ {token.tokenName} - {token.tokenTicker} +

+

+ The repository has been successfully decentralized. A project token has been successfully created. +

+
+
+ +
+ +
+
+
+
+
+
+
+ ) +} diff --git a/src/pages/repository/components/decentralize-modals/Error.tsx b/src/pages/repository/components/decentralize-modals/Error.tsx new file mode 100644 index 00000000..d1ecaaf9 --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/Error.tsx @@ -0,0 +1,37 @@ +import { Button } from '@/components/common/buttons' + +import { ERROR_MESSAGE_TYPES, ErrorMessageTypes } from './config' + +export default function DecentralizeError({ + errorType, + onActionClick +}: { + errorType: ErrorMessageTypes + onActionClick: () => void +}) { + const { title, description, icon: Icon, actionText } = ERROR_MESSAGE_TYPES[errorType] + + return ( + <> +
+
+ +
+

{title || 'Error'}

+

+ {description || 'An error occurred while trying to decentralize the repository.'} +

+
+
+
+ + {onActionClick && ( +
+ +
+ )} + + ) +} diff --git a/src/pages/repository/components/decentralize-modals/LiquidityPoolSuccess.tsx b/src/pages/repository/components/decentralize-modals/LiquidityPoolSuccess.tsx new file mode 100644 index 00000000..cad729cc --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/LiquidityPoolSuccess.tsx @@ -0,0 +1,95 @@ +import { motion } from 'framer-motion' +import { FaPlus } from 'react-icons/fa' + +import { Button } from '@/components/common/buttons' +import { imgUrlFormatter } from '@/helpers/imgUrlFormatter' + +import { CreateLiquidityPoolProps } from './config' + +type Props = { + poolId: string + onClose: () => void + liquidityPoolPayload: CreateLiquidityPoolProps | null +} + + +export default function LiquidityPoolSuccess({ onClose, liquidityPoolPayload, poolId }: Props) { + liquidityPoolPayload + if (!liquidityPoolPayload) return null + + const { tokenA, tokenB, amountA, amountB } = liquidityPoolPayload + return ( + <> +
+
+
+ + +
+
+
+
+

{tokenA.tokenTicker}

+

{amountA}

+
+ +
+

{tokenB.tokenTicker}

+

{amountB}

+
+
+

+ Liquidity Pool for the above tokens has been created successfully. You can trade these tokens now on Bark. + View it on{' '} + + ao.link + + . +

+
+
+
+ +
+ + +
+ + ) +} diff --git a/src/pages/repository/components/decentralize-modals/Loading.tsx b/src/pages/repository/components/decentralize-modals/Loading.tsx new file mode 100644 index 00000000..6dd556fc --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/Loading.tsx @@ -0,0 +1,39 @@ +import { Transition } from '@headlessui/react' +import Progress from '@ramonak/react-progress-bar' +import { Fragment } from 'react' +import Lottie from 'react-lottie' + +import mintLoadingAnimation from '@/assets/coin-minting-loading.json' + +type Props = { + progress: number + text?: string +} + +export default function Loading({ progress = 0, text = 'Processing...' }: Props) { + return ( + +
+
+ +
+
+ +

{text}

+ Do not close or refresh this page +
+
+
+ ) +} diff --git a/src/pages/repository/components/decentralize-modals/MarketStats.tsx b/src/pages/repository/components/decentralize-modals/MarketStats.tsx new file mode 100644 index 00000000..7c0e614d --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/MarketStats.tsx @@ -0,0 +1,31 @@ +import React from 'react' + +interface MarketStatsProps { + marketCap: string + volume: string + circulatingSupply: string +} + +const MarketStats: React.FC = ({ marketCap, volume, circulatingSupply }) => { + return ( +
+

Market Stats

+
+
+ Market Cap + ${marketCap} +
+
+ 24h Volume + {volume} +
+
+ Circulating Supply + {circulatingSupply} +
+
+
+ ) +} + +export default MarketStats diff --git a/src/pages/repository/components/decentralize-modals/Success.tsx b/src/pages/repository/components/decentralize-modals/Success.tsx new file mode 100644 index 00000000..870c2339 --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/Success.tsx @@ -0,0 +1,38 @@ +import { Button } from '@/components/common/buttons' +import { imgUrlFormatter } from '@/helpers/imgUrlFormatter' +import { RepoToken } from '@/types/repository' + +type Props = { + onClose: () => void + token: RepoToken + onAction: () => void +} + +export default function DecentralizeSuccess({ onClose, token, onAction }: Props) { + return ( + <> +
+
+ +
+

+ {token.tokenName} - {token.tokenTicker} +

+

+ The repository has been successfully Tokenized. Now you can trade on the bonding curve. +

+
+
+
+ +
+ + +
+ + ) +} diff --git a/src/pages/repository/components/decentralize-modals/Tokenize-Modal.tsx b/src/pages/repository/components/decentralize-modals/Tokenize-Modal.tsx new file mode 100644 index 00000000..64ecfa2e --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/Tokenize-Modal.tsx @@ -0,0 +1,298 @@ +import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react' +import { Fragment, useEffect, useState } from 'react' +import { toast } from 'react-hot-toast' +import SVG from 'react-inlinesvg' +import { useNavigate } from 'react-router-dom' +import { FadeLoader } from 'react-spinners' + +import CloseCrossIcon from '@/assets/icons/close-cross.svg' +import { waitFor } from '@/helpers/waitFor' +import { decentralizeRepo, initializeBondingCurve, loadTokenProcess } from '@/lib/decentralize' +import { useGlobalStore } from '@/stores/globalStore' +import { BondingCurve, RepoToken } from '@/types/repository' + +import { createConfetti } from '../../helpers/createConfetti' +import { ErrorMessageTypes } from './config' +import Confirm from './Confirm' +import DecentralizeError from './Error' +import Loading from './Loading' +import DecentralizeSuccess from './Success' + +type TokenizeModalProps = { + setIsTradeModalOpen: (open: boolean) => void + onClose: () => void + isOpen: boolean +} + +type DecentralizeStatus = 'IDLE' | 'PENDING' | 'SUCCESS' | 'ERROR' + +export default function TokenizeModal({ setIsTradeModalOpen, onClose, isOpen }: TokenizeModalProps) { + const navigate = useNavigate() + const [isLoading, setIsLoading] = useState(true) + const [repo, setRepoDecentralized] = useGlobalStore((state) => [ + state.repoCoreState.selectedRepo.repo, + state.repoCoreActions.setRepoDecentralized + ]) + + const [tokenizeProgress, setTokenizeProgress] = useState(0) + const [tokenizeProgressText, setTokenizeProgressText] = useState('Tokenizing...') + + const [decentralizeStatus, setDecentralizeStatus] = useState('IDLE') + const [decentralizeError, setDecentralizeError] = useState(null) + + useEffect(() => { + // pollLiquidityProvideMessages('QM8Tc-7yJBGyifsx-DbcDgA3_aGm-p_NOVSYfeGqLwg') + if (repo) { + const validToken = isTokenSettingsValid() + const validBondingCurve = isBondingCurveSettingsValid() + + if (!validToken || !validBondingCurve) { + setDecentralizeError('error-no-token') + } + + setIsLoading(false) + } + }, [repo]) + + function closeModal() { + onClose() + } + + function isTokenSettingsValid() { + if (!repo) return false + + if (!repo.token) { + return false + } + + const requiredFields = ['tokenName', 'tokenTicker', 'denomination', 'totalSupply', 'tokenImage', 'processId'] + for (const field of requiredFields) { + const typedField = field as keyof RepoToken + if (!repo.token[typedField]) { + return false + } + } + + return true + } + + function isBondingCurveSettingsValid() { + if (!repo) return false + + if (!repo.bondingCurve) { + return false + } + + const requiredFields = ['fundingGoal', 'reserveToken', 'processId'] + for (const field of requiredFields) { + const typedField = field as keyof BondingCurve + if (!repo.bondingCurve[typedField]) { + return false + } + } + + return true + } + + async function handleRepoDecentralize() { + if (!repo) return + + if (repo.decentralized && repo.decentralized === true) { + toast.error('Repository is already decentralized.') + return + } + + setDecentralizeStatus('PENDING') + setTokenizeProgress(20) + setTokenizeProgressText('Validating token settings...') + try { + if (!isTokenSettingsValid()) { + setDecentralizeError('error-no-token') + setDecentralizeStatus('ERROR') + + return + } + setTokenizeProgressText('Validating bonding curve settings...') + if (!isBondingCurveSettingsValid()) { + setDecentralizeError('error-no-bonding-curve') + setDecentralizeStatus('ERROR') + + return + } + + await waitFor(1000) + setTokenizeProgress(40) + + setTokenizeProgressText('Creating project token...') + await loadTokenProcess(repo.token!, repo.bondingCurve!.processId!) //loading bonding curve id too + await waitFor(1000) + setTokenizeProgress(60) + setTokenizeProgressText('Creating bonding curve...') + const bondingCurveInitialized = await initializeBondingCurve(repo.token!, repo.bondingCurve!) + if (!bondingCurveInitialized) { + setDecentralizeError('error-generic') + setDecentralizeStatus('ERROR') + return + } + await waitFor(1000) + setTokenizeProgress(80) + setTokenizeProgressText('Tokenizing repository...') + await decentralizeRepo(repo.id) + await waitFor(4000) + createConfetti() + setTimeout(() => { + setDecentralizeStatus('SUCCESS') + setRepoDecentralized() + }, 2000) + setTokenizeProgress(100) + setTokenizeProgressText('Tokenization complete!') + } catch (error) { + toast.error('Failed to tokenize repository.') + setDecentralizeStatus('ERROR') + setDecentralizeError('error-generic') + } + } + + async function handleErrorActions(type: ErrorMessageTypes) { + if (!repo) return + + if (type === 'error-no-token') { + setDecentralizeStatus('IDLE') + setDecentralizeError(null) + navigate(`/repository/${repo.id}/settings/token`) + closeModal() + } + + if (type === 'error-generic') { + setDecentralizeStatus('IDLE') + setDecentralizeError(null) + await handleRepoDecentralize() + } + + if (type === 'error-no-bonding-curve' && repo.bondingCurve) { + setDecentralizeStatus('IDLE') + setDecentralizeError(null) + } + } + + if (!repo) return null + + if (isLoading) + return ( + + {}}> + +
+ +
+
+ + +
+ + Repository Tokenization + + +
+
+ +
+
+
+
+
+
+
+ ) + return ( + + {}}> + +
+ + +
+
+ + +
+ + Repository Tokenization + + +
+ {!decentralizeError && decentralizeStatus === 'PENDING' && ( + + )} + + {decentralizeError && ( + handleErrorActions(decentralizeError)} + /> + )} + + {decentralizeStatus === 'SUCCESS' && ( + { + setIsTradeModalOpen(true) + }} + token={repo.token!} + /> + )} + {!decentralizeError && decentralizeStatus === 'IDLE' && ( + + )} + {/* {decentralizeStatus === 'LIQUIDITY_POOL' && repo?.liquidityPool && ( + + )} */} +
+
+
+
+
+
+ ) +} diff --git a/src/pages/repository/components/decentralize-modals/Tokenized-LiquidityPool.tsx b/src/pages/repository/components/decentralize-modals/Tokenized-LiquidityPool.tsx new file mode 100644 index 00000000..1b883749 --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/Tokenized-LiquidityPool.tsx @@ -0,0 +1,294 @@ +import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react' +import numeral from 'numeral' +import { Fragment, useEffect, useState } from 'react' +import toast from 'react-hot-toast' +import { HiChevronUpDown } from 'react-icons/hi2' +import { IoCheckmark } from 'react-icons/io5' + +import { Button } from '@/components/common/buttons' +import { fetchAllUserTokens } from '@/helpers/wallet/fetchAllTokens' +import { fetchTokenBalance } from '@/lib/decentralize' +import { useGlobalStore } from '@/stores/globalStore' +import { RepoLiquidityPool, RepoLiquidityPoolToken } from '@/types/repository' + +import { CreateLiquidityPoolProps } from './config' + +type Props = { + onClose: () => void + liquidityPool: RepoLiquidityPool + onAction: (payload: CreateLiquidityPoolProps) => Promise +} + +// const USDA_TST = { +// tokenName: 'Astro USD (Test)', +// tokenTicker: 'USDA-TST', +// processId: 'GcFxqTQnKHcr304qnOcq00ZqbaYGDn4Wbb0DHAM-wvU', +// denomination: '12', +// tokenImage: 'K8nurc9H0_ZQm17jbs3ryEs6MrlX-oIK_krpprWlQ-Q' +// } + +const QAR = { + tokenName: 'Q Arweave', + tokenTicker: 'qAR', + // processId: 'b87Jd4usKGyMjovbNeX4P3dcvkC4mrtBZ5HxW_ENtn4', + processId: 'NG-0lVX882MG5nhARrSzyprEK6ejonHpdUmaaMPsHE8', + denomination: '12', + tokenImage: '26yDr08SuwvNQ4VnhAfV4IjJcOOlQ4tAQLc1ggrCPu0' +} + +export default function TokenizedLiquidityPool({ onClose, liquidityPool, onAction }: Props) { + const [errors, setErrors] = useState<{ + baseTokenAmount: string + quoteTokenAmount: string + }>({ + baseTokenAmount: '', + quoteTokenAmount: '' + }) + const [address] = useGlobalStore((state) => [state.authState.address]) + const [isTokenListLoading, setIsTokenListLoading] = useState(true) + const [tokenList, setTokenList] = useState<(typeof QAR)[]>([QAR]) + const [selectedQuoteToken, setSelectedQuoteToken] = useState(() => { + if (liquidityPool) { + return liquidityPool.quoteToken + } + return QAR + }) + const [baseTokenAmount, setBaseTokenAmount] = useState('') + const [baseTokenBalance, setBaseTokenBalance] = useState(0) + const [quoteTokenAmount, setQuoteTokenAmount] = useState('') + const [quoteTokenBalance, setQuoteTokenBalance] = useState(0) + + useEffect(() => { + fetchAllUserTokens().then((tokens) => { + setTokenList(tokens) + setIsTokenListLoading(false) + }) + if (liquidityPool) { + getBalance(liquidityPool.baseToken, liquidityPool.quoteToken) + } + }, []) + + useEffect(() => { + if (liquidityPool) { + getBalance(liquidityPool.baseToken, liquidityPool.quoteToken) + setSelectedQuoteToken(liquidityPool.quoteToken) + } + }, []) + + useEffect(() => { + if (selectedQuoteToken) { + fetchTokenBalance(selectedQuoteToken.processId, address!).then((balance) => { + setQuoteTokenBalance(+balance / 10 ** +selectedQuoteToken.denomination) + }) + } + }, [selectedQuoteToken]) + + useEffect(() => { + if (parseFloat(baseTokenAmount) > baseTokenBalance) { + setErrors((prev) => ({ ...prev, baseTokenAmount: 'Balance is too low' })) + } else { + setErrors((prev) => ({ ...prev, baseTokenAmount: '' })) + } + + if (parseFloat(quoteTokenAmount) > quoteTokenBalance) { + setErrors((prev) => ({ ...prev, quoteTokenAmount: 'Balance is too low' })) + } else { + setErrors((prev) => ({ ...prev, quoteTokenAmount: '' })) + } + }, [baseTokenAmount, baseTokenBalance, quoteTokenAmount, quoteTokenBalance]) + + async function getBalance(baseToken: RepoLiquidityPoolToken, quoteToken: RepoLiquidityPoolToken) { + const baseTokenBalance = await fetchTokenBalance(baseToken.processId, address!) + const quoteTokenBalance = await fetchTokenBalance(quoteToken.processId, address!) + + setBaseTokenBalance(+baseTokenBalance / 10 ** +baseToken.denomination) + setQuoteTokenBalance(+quoteTokenBalance / 10 ** +quoteToken.denomination) + } + + async function handleSubmit() { + if (!baseTokenAmount || !quoteTokenAmount) { + toast.error('Please enter an amount for both tokens') + return + } + + if (errors.baseTokenAmount || errors.quoteTokenAmount) { + return + } + + try { + onAction({ + tokenA: liquidityPool.baseToken, + tokenB: selectedQuoteToken, + amountA: baseTokenAmount, + amountB: quoteTokenAmount, + balanceA: baseTokenBalance.toString(), + balanceB: quoteTokenBalance.toString() + }) + } catch (error) { + console.log({ error }) + } + } + + const { baseToken } = liquidityPool + return ( + <> +
+
+
+
+ +
+

+ Balance: {numeral(baseTokenBalance).format('0')} +

+
+
+ +
+ + {baseToken?.tokenTicker} + + + + + + {tokenList.map((token) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active ? 'bg-primary-100' : 'text-gray-900' + }` + } + value={token} + > + {({ selected }) => ( + <> + + {token.tokenTicker} + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
+
+
+
+ + { + const value = e.target.value + const regex = /^[0-9]*[.]?[0-9]*$/ + if (regex.test(value)) { + setBaseTokenAmount(value) + } + }} + type="text" + className="outline-none w-full bg-transparent text-3xl" + placeholder="0" + /> + {errors.baseTokenAmount &&

{errors.baseTokenAmount}

} +
+
+
+
+
+ +
+

+ Balance: {numeral(quoteTokenBalance).format('0')} +

+
+
+ setSelectedQuoteToken(value)}> +
+ + + {isTokenListLoading ? 'Loading...' : selectedQuoteToken?.tokenTicker} + + + + + + + {tokenList.map((token) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active ? 'bg-primary-100' : 'text-gray-900' + }` + } + value={token} + > + {({ selected }) => ( + <> + + {token.tokenTicker} + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
+
+
+
+ + { + const value = e.target.value + const regex = /^[0-9]*[.]?[0-9]*$/ + if (regex.test(value)) { + setQuoteTokenAmount(value) + } + }} + type="text" + className="outline-none w-full bg-transparent text-3xl" + placeholder="0" + /> + {errors.quoteTokenAmount &&

{errors.quoteTokenAmount}

} +
+
+
+ +
+ + +
+ + ) +} diff --git a/src/pages/repository/components/decentralize-modals/Trade-Modal.tsx b/src/pages/repository/components/decentralize-modals/Trade-Modal.tsx new file mode 100644 index 00000000..bfba9cf1 --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/Trade-Modal.tsx @@ -0,0 +1,513 @@ +import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react' +import Progress from '@ramonak/react-progress-bar' +import clsx from 'clsx' +import { Fragment } from 'react' +import React from 'react' +import { toast } from 'react-hot-toast' +import SVG from 'react-inlinesvg' + +import CloseCrossIcon from '@/assets/icons/close-cross.svg' +import { Button } from '@/components/common/buttons' +import { imgUrlFormatter } from '@/helpers/imgUrlFormatter' +import { parseScientific } from '@/helpers/parseScientific' +import { shortenAddress } from '@/helpers/shortenAddress' +import { debounce } from '@/helpers/withDebounce' +import { getBuySellTransactionsOfCurve, getTokenBuyPrice } from '@/lib/bonding-curve' +import { buyTokens } from '@/lib/bonding-curve/buy' +import { getCurveState, getTokenCurrentSupply } from '@/lib/bonding-curve/helpers' +import { calculateTokensSellCost, sellTokens } from '@/lib/bonding-curve/sell' +import { fetchTokenBalance, fetchTokenBalances } from '@/lib/decentralize' +import { useGlobalStore } from '@/stores/globalStore' +import { CurveState } from '@/stores/repository-core/types' +import { RepoToken } from '@/types/repository' + +import { roundToSignificantFigures } from '../../helpers/roundToSigFigures' +import TradeChartComponent from './TradeChartComponent' +import TransferToLP from './TransferToLP' + +type TradeModalProps = { + onClose: () => void + isOpen: boolean +} + +type ChartData = { + time: string | number + value: string | number +} + +export default function TradeModal({ onClose, isOpen }: TradeModalProps) { + const amountRef = React.useRef(null) + const [chartData, setChartData] = React.useState([]) + const [balances, setBalances] = React.useState>({}) + const [transactionPending, setTransactionPending] = React.useState(false) + const [curveState, setCurveState] = React.useState({} as CurveState) + const [price, setPrice] = React.useState('0') + const [priceUnscaled, setPriceUnscaled] = React.useState('0') + const [reserveTokenBalance, setReserveTokenBalance] = React.useState('0') + const [repoTokenBalance, setRepoTokenBalance] = React.useState('0') + const [amount, setAmount] = React.useState('') + const [tokenPair, setTokenPair] = React.useState([]) + const [selectedTokenToTransact, setSelectedTokenToTransact] = React.useState(0) + const [selectedSide, setSelectedSide] = React.useState<'buy' | 'sell'>('buy') + const [repo, address] = useGlobalStore((state) => [state.repoCoreState.selectedRepo.repo, state.authState.address]) + const [progress, setProgress] = React.useState(0) + React.useEffect(() => { + if (repo && repo.token && repo.bondingCurve && repo.bondingCurve.reserveToken) { + setTokenPair([repo.token, repo.bondingCurve.reserveToken as RepoToken]) + setSelectedTokenToTransact(0) + handleGetCurveState() + handleGetTokenBalances() + handleGetTransactions() + } + }, [repo]) + console.log({ reserveTokenBalance }) + React.useEffect(() => { + setAmount('') + }, [selectedSide]) + + React.useEffect(() => { + if (curveState.maxSupply && curveState.fundingGoal) { + setProgress((+curveState.reserveBalance / +curveState.fundingGoal) * 100) + handleGetTokenHoldersBalances() + } + }, [curveState]) + + React.useEffect(() => { + if (amount) { + handleGetBuyPrice() + } + }, [amount]) + + async function handleGetTokenBalances() { + if (!repo || !address || !repo.bondingCurve || !repo.token) return + const reserveTokenBalance = await fetchTokenBalance(repo.bondingCurve.reserveToken.processId!, address!) + const repoTokenBalance = await fetchTokenBalance(repo.token.processId!, address!) + setReserveTokenBalance((+reserveTokenBalance / 10 ** +repo.bondingCurve.reserveToken.denomination).toString()) + setRepoTokenBalance((+repoTokenBalance / 10 ** +repo.token.denomination).toString()) + } + + async function handleGetTokenHoldersBalances() { + if (!repo || !repo.token) return + const balances = await fetchTokenBalances(repo.token.processId!) + if (!curveState.maxSupply) return + + // Convert raw balances to percentages of max supply + const balancesAsPercentages = Object.entries(balances).reduce( + (acc, [address, amount]) => { + const percentage = ((Number(amount) / Number(curveState.maxSupply)) * 100).toFixed(2) + acc[address] = percentage + return acc + }, + {} as Record + ) + setBalances(balancesAsPercentages) + } + + async function handleGetTransactions() { + if (!repo || !repo.bondingCurve || !repo.token) return + + const chart: ChartData[] = [] + + const transactions = await getBuySellTransactionsOfCurve(repo.bondingCurve.processId!) + let lastTimestamp = 0 + transactions.forEach((transaction: any) => { + const costTag = transaction.node.tags.find((tag: any) => tag.name === 'Cost') + const tokensSoldTag = transaction.node.tags.find((tag: any) => tag.name === 'TokensSold') + const tokensBoughtTag = transaction.node.tags.find((tag: any) => tag.name === 'TokensBought') + + const cost = costTag ? parseInt(costTag.value) : 0 + const tokensSold = tokensSoldTag ? parseInt(tokensSoldTag.value) : 0 + const tokensBought = tokensBoughtTag ? parseInt(tokensBoughtTag.value) : 0 + let price = 0 + // Calculate price based on transaction type + if (tokensBought > 0) { + // Buy transaction: Price = Cost / TokensBought + price = cost / tokensBought + } else if (tokensSold > 0) { + // Sell transaction: Price = Proceeds / TokensSold + price = cost / tokensSold + } else { + // If no tokens were bought or sold, skip this transaction as it doesn't affect the price + return + } + + let timestamp = transaction.node.ingested_at || 0 + // Ensure timestamps are incrementing + if (timestamp <= lastTimestamp) { + timestamp = lastTimestamp + 1 + } + lastTimestamp = timestamp + + // const timestamp = transaction.node.ingested_at || 0 + chart.push({ + time: timestamp, + value: price + }) + }) + setChartData(chart) + } + + async function handleGetCurveState() { + if (!repo?.bondingCurve) return + + const state = await getCurveState(repo.bondingCurve.processId!) + setCurveState(state) + } + + function handleTokenSwitch() { + toast.success('Coming soon') + return + if (!repo) return + if (selectedTokenToTransact === 0) { + setSelectedTokenToTransact(1) + } else { + setSelectedTokenToTransact(0) + } + } + + function closeModal() { + onClose() + } + + const debouncedSetAmount = React.useCallback( + debounce((value: string) => { + setAmount(value) + }, 500), + [] + ) + + function handleAmountChange(e: React.ChangeEvent) { + debouncedSetAmount(e.target.value) + } + + async function handleGetBuyPrice() { + if (!amount || !repo?.bondingCurve || !curveState.maxSupply || !curveState.fundingGoal) return + + let price = 0 + const currentSupply = await getTokenCurrentSupply(repo.token!.processId!) + if (selectedSide === 'buy') { + const tokensToBuy = +amount + // price = await calculateTokensBuyCost( + // repo.token!.processId!, + // tokensToBuy, + // +curveState.maxSupply, + // +curveState.fundingGoal + // ) + + price = await getTokenBuyPrice(tokensToBuy.toString(), currentSupply, repo.bondingCurve.processId!) + } + if (selectedSide === 'sell') { + const tokensToSell = +amount * 10 ** +repo.token!.denomination + price = await calculateTokensSellCost( + repo.token!.processId!, + tokensToSell, + +curveState.fundingGoal, + +curveState.supplyToSell + ) + } + setPriceUnscaled(price.toString()) + const priceInReserveTokens = price / 10 ** +repo.bondingCurve.reserveToken.denomination + const formattedPrice = parseScientific( + roundToSignificantFigures(priceInReserveTokens, +repo.bondingCurve.reserveToken.denomination).toString() + ) + + setPrice(formattedPrice || '') + } + + async function handleSelectedSideChange(side: 'buy' | 'sell') { + setSelectedSide(side) + setAmount('') + amountRef.current!.value = '0' + setPrice('0') + } + + async function handleSideAction() { + if (selectedSide === 'buy') { + await handleBuy() + } + if (selectedSide === 'sell') { + await handleSell() + } + + await handleGetTokenBalances() + await handleGetCurveState() + await handleGetTransactions() + } + + async function handleBuy() { + if (!repo || !repo.bondingCurve || !repo.bondingCurve.reserveToken) return + if (+amount <= 0) return + if (+price <= 0) return + try { + setTransactionPending(true) + // const currentSupply = await getTokenCurrentSupply(repo.token!.processId!) + + const { success } = await buyTokens( + repo.bondingCurve.processId!, + repo.bondingCurve.reserveToken.processId!, + amount, + priceUnscaled + ) + if (success) { + toast.success('Tokens bought successfully.') + setAmount('') + amountRef.current!.value = '' + } + + if (!success) { + toast.error('Error buying tokens. Try again later.') + } + } catch (error) { + toast.error('Error buying tokens. Try again later.') + } finally { + setTransactionPending(false) + } + } + + async function handleSell() { + if (!repo || !repo.bondingCurve || !repo.bondingCurve.reserveToken) return + if (+amount <= 0) return + if (+price <= 0) return + try { + setTransactionPending(true) + const { success } = await sellTokens(repo.bondingCurve.processId!, amount) + if (success) { + toast.success('Tokens sold successfully.') + setAmount('') + amountRef.current!.value = '' + } + + if (!success) { + toast.error('Error selling tokens. Try again later.') + } + } catch (error) { + toast.error('Error selling tokens. Try again later.') + } finally { + setTransactionPending(false) + } + } + + function parseReserveBalance() { + if (!repo?.bondingCurve?.reserveToken || !curveState.reserveBalance) return 0 + return parseFloat(curveState.reserveBalance) / 10 ** +(repo?.bondingCurve?.reserveToken?.denomination || 0) + } + + const selectedToken = tokenPair[selectedTokenToTransact] + if (!repo || !selectedToken) return null + console.log({ curveState }) + return ( + + + +
+ + +
+
+ + +
+ + {repo.token?.tokenTicker}/{repo.bondingCurve?.reserveToken?.tokenTicker} + + +
+ +
+ {/* TradingView Chart Section - 70% */} +
+ +
+ + {/* Buy/Sell Widget Section - 30% */} + {curveState && curveState.reachedFundingGoal ? ( + + ) : ( +
+
+

Funding Goal Progress

+ +
+

Reserve Balance

+

+ {parseReserveBalance()} {repo?.bondingCurve?.reserveToken.tokenTicker} +

+
+
+
+
+ + +
+ +
+
+
+ +
+
+ Switch to $ + {selectedTokenToTransact === 0 + ? tokenPair[1]?.tokenTicker + : tokenPair[0]?.tokenTicker} +
+
+
+
+ +
+ {selectedToken?.tokenTicker} + +
+
+
+ + {price && selectedSide === 'buy' && ( +
+ +
+ {price}{' '} + {selectedSide === 'buy' + ? repo?.bondingCurve?.reserveToken.tokenTicker + : repo?.token?.tokenTicker} + +
+
+ )} + {selectedSide === 'buy' && ( +
+ +
+ {amount} {repo?.token?.tokenTicker}{' '} + +
+
+ )} + {/* {selectedSide === 'buy' && ( +
+ +
+ {reserveTokenBalance} {repo?.bondingCurve?.reserveToken.tokenTicker}{' '} + +
+
+ )} */} + {selectedSide === 'sell' && ( +
+ +
+ {repoTokenBalance} {repo?.token?.tokenTicker}{' '} + +
+
+ )} + {selectedSide === 'sell' && ( +
+ +
+ {price} {repo?.bondingCurve?.reserveToken.tokenTicker} + +
+
+ )} + +
+
+
+ +
+ {Object.entries(balances).map(([balAddress, percentage], index) => ( +
+ + {index + 1}. {shortenAddress(balAddress, 6)}{' '} + {repo.bondingCurve?.processId === balAddress ? '(Bonding Curve)' : ''} + {address === balAddress ? '(Creator)' : ''} + + {percentage}% +
+ ))} +
+
+
+ )} +
+
+
+
+
+
+
+ ) +} diff --git a/src/pages/repository/components/decentralize-modals/TradeChartComponent.tsx b/src/pages/repository/components/decentralize-modals/TradeChartComponent.tsx new file mode 100644 index 00000000..6f573bf9 --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/TradeChartComponent.tsx @@ -0,0 +1,197 @@ +import { ColorType, createChart } from 'lightweight-charts' +import React from 'react' + +import { getTokenCurrentSupply } from '@/lib/bonding-curve/helpers' +import { Repo } from '@/types/repository' + +import MarketStats from './MarketStats' + +const styles = { + backgroundColor: 'white', + lineColor: 'white', + textColor: 'black', + areaTopColor: 'rgb(86 173 217)', + areaBottomColor: 'rgba(86 173 217/0.28)', + tooltipWrapper: { + background: 'rgb(86 173 217)', + border: 'none' + }, + tooltip: { + background: 'rgb(86 173 217)', + color: 'rgb(86 173 217)' + } +} + +type ChartData = { + time: string | number + value: string | number +} + +interface MarketStatsProps { + marketCap: string + volume: string + circulatingSupply: string + reserveBalance: string +} + +export default function TradeChartComponent({ + data, + repo, + reserveBalance +}: { + data: ChartData[] + repo: Repo + reserveBalance: string +}) { + const [stats, setStats] = React.useState({ + marketCap: '0', + volume: '0', + circulatingSupply: '0', + reserveBalance: '0' + }) + const chartContainerRef = React.useRef(null) + // React.useEffect(() => { + // if (!chartContainerRef.current) return + // setSize({ + // width: chartContainerRef.current.clientWidth, + // height: chartContainerRef.current.clientHeight + // }) + // }, [chartContainerRef]) + + React.useEffect(() => { + fetchStats() + }, [data, reserveBalance]) + + async function fetchStats() { + if (!repo?.token?.processId) return + const stats = await getTokenCurrentSupply(repo?.token?.processId) + + const normalizedSupply = (Number(stats) / 10 ** +repo?.token?.denomination).toFixed(2) + calculateMarketCap(reserveBalance, normalizedSupply) + } + + function calculateMarketCap(reserves: string, currentSupply: string) { + // Convert the string inputs to numbers + const reservesAmount = parseFloat(reserves) + const supplyAmount = parseFloat(currentSupply) + + // Ensure that currentSupply is not zero to avoid division by zero + if (supplyAmount === 0 || reservesAmount === 0) { + setStats({ + marketCap: '0', + volume: '0', + circulatingSupply: currentSupply, + reserveBalance: reserveBalance + }) + } + + // Calculate token price based on reserves and current supply + const tokenPrice = reservesAmount / supplyAmount + + // Calculate market cap by multiplying token price with current supply + const marketCap = (tokenPrice * supplyAmount).toFixed(2) + + setStats({ + marketCap: marketCap, + volume: '0', + circulatingSupply: currentSupply, + reserveBalance: reserveBalance + }) + } + + // function formatYAxis(value: number) { + // return value.toFixed(10) + // } + + React.useEffect(() => { + if (!chartContainerRef.current) return + const handleResize = () => { + if (!chartContainerRef.current) return + + chart.applyOptions({ width: chartContainerRef.current.clientWidth }) + } + + const chart = createChart(chartContainerRef.current, { + layout: { + background: { type: ColorType.Solid, color: styles.backgroundColor }, + textColor: styles.textColor + }, + grid: { + vertLines: { + color: '#eee' + }, + horzLines: { + color: '#eee' + } + }, + rightPriceScale: { + visible: true, + borderVisible: false, + scaleMargins: { + bottom: 0 + } + }, + width: chartContainerRef.current.clientWidth, + height: chartContainerRef.current.clientHeight, + timeScale: { + // rightOffset: 0, + fixRightEdge: true, + borderVisible: false, + timeVisible: true, + secondsVisible: false + } + }) + chart.timeScale().fitContent() + + const newSeries = chart.addAreaSeries({ + lineColor: styles.lineColor, + topColor: styles.areaTopColor, + bottomColor: styles.areaBottomColor, + crosshairMarkerBackgroundColor: styles.tooltip.background, + priceFormat: { + type: 'price', + precision: 12, + minMove: 0.000001 + } + }) + // const lineSeries = chart.addLineSeries({ + // priceFormat: { + // type: 'price', + // precision: 12, // Number of decimal places to display + // minMove: 0.000001 // Minimum price movement + // } + // }) + newSeries.setData(data as any) + + window.addEventListener('resize', handleResize) + + return () => { + window.removeEventListener('resize', handleResize) + + chart.remove() + } + }, [data]) + + return ( +
+
+ {!data.length && ( +
+

No data

+
+ )} +
+ {/* + + + `${value}`} /> + + */} +
+ +
+ ) +} diff --git a/src/pages/repository/components/decentralize-modals/TradingSide.tsx b/src/pages/repository/components/decentralize-modals/TradingSide.tsx new file mode 100644 index 00000000..3b000868 --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/TradingSide.tsx @@ -0,0 +1,47 @@ +import React from 'react' + +import { RepoToken } from '@/types/repository' + +type Props = { + handleTokenSwitch: () => void + handleAmountChange: (e: React.ChangeEvent) => void + selectedTokenToTransact: number + tokenPair: RepoToken[] + selectedToken: RepoToken +} + +export default function TradingSide({ + handleTokenSwitch, + handleAmountChange, + selectedTokenToTransact, + tokenPair, + selectedToken +}: Props) { + return ( +
+
+ +
+
+ Switch to ${selectedTokenToTransact === 0 ? tokenPair[1]?.tokenTicker : tokenPair[0]?.tokenTicker} +
+
+
+
+ +
+ {selectedToken?.tokenTicker} + {/* */} +
+
+
+ ) +} diff --git a/src/pages/repository/components/decentralize-modals/TransferToLP.tsx b/src/pages/repository/components/decentralize-modals/TransferToLP.tsx new file mode 100644 index 00000000..e854dfac --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/TransferToLP.tsx @@ -0,0 +1,211 @@ +import { useState } from 'react' +import toast from 'react-hot-toast' +import { BsFillPatchCheckFill } from 'react-icons/bs' +import { MdError } from 'react-icons/md' + +import { Button } from '@/components/common/buttons' +import { waitFor } from '@/helpers/waitFor' +import { checkLiquidityPoolReserves, createLiquidityPool } from '@/lib/bark' +import { depositToLPFromBondingCurve } from '@/lib/bonding-curve' +import { useGlobalStore } from '@/stores/globalStore' +import { CurveState } from '@/stores/repository-core/types' +import { RepoLiquidityPoolToken } from '@/types/repository' + +import Loading from './Loading' + +export default function TransferToLP({ + getCurveState, + curveState +}: { + getCurveState: () => void + curveState: CurveState +}) { + const [text, setText] = useState('Loading...') + const [status, setStatus] = useState<'IDLE' | 'SUCCESS' | 'PENDING' | 'ERROR'>('IDLE') + const [address, repo] = useGlobalStore((state) => [state.authState.address, state.repoCoreState.selectedRepo.repo]) + const [tokenizeProgress, setTokenizeProgress] = useState(0) + + async function transferToLP() { + if (!repo) return + console.log('transfer to lp') + setStatus('PENDING') + setText('Transferring to Botega...') + setTokenizeProgress(20) + const tokenA = repo.token! + const tokenB = repo.bondingCurve!.reserveToken! + try { + const data = await createLiquidityPool({ + tokenA: tokenA as RepoLiquidityPoolToken, + tokenB: tokenB + }) + + if (data.poolStatus === 'ERROR' && data.message) { + setStatus('ERROR') + toast.error(data.message) + return + } + setTokenizeProgress(60) + setText('Finalizing transfer to Botega...') + waitFor(500) + + const depositStatus = await depositToLPFromBondingCurve(data.poolId, repo.bondingCurve!.processId!) + if (!depositStatus.success) { + setStatus('ERROR') + toast.error(depositStatus.message) + return + } + + setTokenizeProgress(80) + setText('Checking liquidity pool status...') + + const reserves = await checkLiquidityPoolReserves(data.poolId) + + if (!reserves) { + setStatus('ERROR') + toast.error('An error occurred while checking the liquidity pool status') + return + } + console.log(reserves) + + const tokenAQty = parseInt(curveState.maxSupply) * 0.2 + const tokenBQty = parseInt(curveState.reserveBalance) + let reservesHasError = false + const expectedReserves = { + [tokenA.processId!]: tokenAQty, + [tokenB.processId]: tokenBQty + } + + if (Object.keys(reserves).length !== 2) { + reservesHasError = true + } else { + for (const [processId, expectedAmount] of Object.entries(expectedReserves)) { + if (!(processId in reserves) || +reserves[processId] !== expectedAmount) { + reservesHasError = true + break + } + } + } + + if (reservesHasError) { + setStatus('ERROR') + toast.error('An error occurred while checking the liquidity pool status') + return + } + + setStatus('SUCCESS') + toast.success('Liquidity pool created and liquidity transferred') + + setTimeout(() => { + setStatus('IDLE') + }, 2500) + getCurveState() + } catch (error) { + setStatus('ERROR') + toast.error('An error occurred while creating the liquidity pool') + } + } + + function handleTradeOnBotega() { + window.open(`https://botega.arweave.dev/`, '_blank') + } + + if (!repo) return null + console.log(repo.owner, address) + + if (status === 'PENDING') { + return ( +
+ +
+ ) + } + + if (status === 'ERROR') { + return ( +
+ {/* */} +
+ +

An error occurred

+
+

+ An error occurred while transferring liquidity to Botega. Please try again. +

+ +
+ ) + } + + if (status === 'SUCCESS') { + return ( +
+ {/* */} +
+ +

Reserves transferred

+
+

+ Liquidity pool created and liquidity transferred to Botega. +

+ +
+ ) + } + return ( +
+
+ +

Funding goal reached

+
+ {repo.owner !== address && !curveState.liquidityPool && ( +
+

Moving Reserves to Botega

+

+ This project is now in the process of moving liquidity to Botega. Please check back later. +

+
+ )} + {repo.owner === address && !curveState.liquidityPool && ( +
+

Transfer to Botega

+

+ Congratulations! You can now transfer liquidity to Botega. +

+ +
+ )} + + {curveState.liquidityPool && ( +
+

Trade on Botega

+

Congratulations! You can now trade on Botega.

+ +
+ )} +
+ ) +} diff --git a/src/pages/repository/components/decentralize-modals/config.ts b/src/pages/repository/components/decentralize-modals/config.ts new file mode 100644 index 00000000..e408d068 --- /dev/null +++ b/src/pages/repository/components/decentralize-modals/config.ts @@ -0,0 +1,35 @@ +import { MdError } from 'react-icons/md' + +import { RepoLiquidityPoolToken } from '@/types/repository' + +export const ERROR_MESSAGE_TYPES = { + 'error-generic': { + title: 'Error', + description: 'An error occurred while trying to tokenize the repository.', + icon: MdError, + actionText: 'Try Again' + }, + 'error-no-token': { + title: 'Incomplete Token Settings', + description: 'You need to complete the token settings to tokenize the repository.', + icon: MdError, + actionText: 'Complete Token Settings' + }, + 'error-no-bonding-curve': { + title: 'Incomplete Bonding Curve Settings', + description: 'You need to complete the bonding curve settings to tokenize the repository.', + icon: MdError, + actionText: 'Complete Bonding Curve Settings' + } +} + +export type ErrorMessageTypes = keyof typeof ERROR_MESSAGE_TYPES + +export type CreateLiquidityPoolProps = { + tokenA: RepoLiquidityPoolToken + tokenB: RepoLiquidityPoolToken + amountA?: string + amountB?: string + balanceA?: string + balanceB?: string +} diff --git a/src/pages/repository/components/tabs/code-tab/FileView.tsx b/src/pages/repository/components/tabs/code-tab/FileView.tsx index 5d6a1b82..a32c45e0 100644 --- a/src/pages/repository/components/tabs/code-tab/FileView.tsx +++ b/src/pages/repository/components/tabs/code-tab/FileView.tsx @@ -13,7 +13,6 @@ import { MdOutlineEdit } from 'react-icons/md' import Sticky from 'react-stickynode' import { Button } from '@/components/common/buttons' -import rehypeAnchorOnClick from '@/helpers/rehypeAnchorOnClickPlugin' import { isImage, isMarkdown } from '@/pages/repository/helpers/filenameHelper' import useLanguage from '@/pages/repository/hooks/useLanguage' import { useGlobalStore } from '@/stores/globalStore' @@ -177,11 +176,7 @@ export default function FileView({ fileContent, setFileContent, filename, setFil /> ) : ( - + ) ) : !isMarkdownFile ? ( setFileContent((content) => ({ ...content, modified: value! }))} /> ) : ( - + )}
diff --git a/src/pages/repository/components/tabs/code-tab/Readme.tsx b/src/pages/repository/components/tabs/code-tab/Readme.tsx index 505da123..dd3bbcea 100644 --- a/src/pages/repository/components/tabs/code-tab/Readme.tsx +++ b/src/pages/repository/components/tabs/code-tab/Readme.tsx @@ -4,7 +4,6 @@ import React, { useEffect, useState } from 'react' import { LiaReadme } from 'react-icons/lia' import Sticky from 'react-stickynode' -import rehypeAnchorOnClickPlugin from '@/helpers/rehypeAnchorOnClickPlugin' import { getFileContent } from '@/pages/repository/helpers/filenameHelper' import { useGlobalStore } from '@/stores/globalStore' import { FileObject } from '@/stores/repository-core/types' @@ -55,11 +54,7 @@ export default function Readme({ fileObject }: { fileObject?: FileObject }) {
README
- +
) } diff --git a/src/pages/repository/components/tabs/forks-tab/CustomNode.tsx b/src/pages/repository/components/tabs/forks-tab/CustomNode.tsx new file mode 100644 index 00000000..18e28650 --- /dev/null +++ b/src/pages/repository/components/tabs/forks-tab/CustomNode.tsx @@ -0,0 +1,50 @@ +import { Handle, Position } from '@xyflow/react' +import clsx from 'clsx' +import { memo } from 'react' + +import { TreeNodeData } from './types' + +export const CustomNode = memo(({ data }: { data: TreeNodeData }) => { + function handleNodeClick() { + window.open(`/#/repository/${data.id}`, '_blank') + } + return ( +
+
+
SK
+
+
{data.name}
+
{data.description || 'No description'}
+
+ +
+ {data.origin &&
Origin
} + {data.isCurrentRepo &&
You
} +
+
+
+ {data.primary && ( +
Primary
+ )} +
+ {data.decentralized ? 'Decentralized' : 'Centralized'} +
+ {data.decentralized && data.token && data.token.processId && ( +
+ ${data.token.tokenTicker} +
+ )} +
+ + +
+ ) +}) diff --git a/src/pages/repository/components/tabs/forks-tab/RepoTreeMap.tsx b/src/pages/repository/components/tabs/forks-tab/RepoTreeMap.tsx new file mode 100644 index 00000000..b00761a4 --- /dev/null +++ b/src/pages/repository/components/tabs/forks-tab/RepoTreeMap.tsx @@ -0,0 +1,46 @@ +import '@xyflow/react/dist/base.css' + +import { addEdge, ReactFlow, useEdgesState, useNodesState, useReactFlow } from '@xyflow/react' +import React from 'react' +import { useCallback } from 'react' + +import { useGlobalStore } from '@/stores/globalStore' + +import { CustomNode } from './CustomNode' + +const nodeTypes = { + custom: CustomNode +} + +export default function RepoTreeMap() { + const reactFlow = useReactFlow() + const [repoHierarchy, fetchRepoHierarchy] = useGlobalStore((state) => [ + state.repoCoreState.selectedRepo.repoHierarchy, + state.repoCoreActions.fetchRepoHierarchy + ]) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [nodes, _, onNodesChange] = useNodesState(repoHierarchy.nodes) + const [edges, setEdges, onEdgesChange] = useEdgesState(repoHierarchy.edges) + const onConnect = useCallback((params: any) => setEdges((els) => addEdge(params, els)), []) + React.useEffect(() => { + reactFlow.setViewport({ x: 10, y: 200, zoom: 0.7 }) + }, [reactFlow]) + + React.useEffect(() => { + fetchRepoHierarchy() + }, []) + + return ( + <> + + + ) +} diff --git a/src/pages/repository/components/tabs/forks-tab/index.tsx b/src/pages/repository/components/tabs/forks-tab/index.tsx index 6d93508c..0b3413a5 100644 --- a/src/pages/repository/components/tabs/forks-tab/index.tsx +++ b/src/pages/repository/components/tabs/forks-tab/index.tsx @@ -1,65 +1,15 @@ -import { formatDistanceToNow } from 'date-fns' -import { FiGitBranch, FiGitCommit } from 'react-icons/fi' -import { Link } from 'react-router-dom' +import '@xyflow/react/dist/base.css' -import { resolveUsernameOrShorten } from '@/helpers/resolveUsername' -import { useGlobalStore } from '@/stores/globalStore' +import { ReactFlowProvider } from '@xyflow/react' -export default function ForksTab() { - const [userRepo] = useGlobalStore((state) => [state.repoCoreState.selectedRepo.repo]) - const forks = userRepo?.forks ?? {} - - if (Object.values(forks).length === 0) { - return ( -
-
-
- -
-
- -

Looks like this repository hasn't been forked yet.

-
-
-
-
- ) - } +import RepoTreeMap from './RepoTreeMap' +export default function ForksTab() { return ( -
- {Object.values(forks).map((fork) => ( -
-
- -
-
-
- - {fork.id}/{fork.name} - -
- - {resolveUsernameOrShorten(fork.owner)} - - created a fork - - {formatDistanceToNow(new Date(fork.timestamp), { addSuffix: true })} - -
-
-
-
- ))} +
+ + +
) } diff --git a/src/pages/repository/components/tabs/forks-tab/types.ts b/src/pages/repository/components/tabs/forks-tab/types.ts new file mode 100644 index 00000000..9e1cae51 --- /dev/null +++ b/src/pages/repository/components/tabs/forks-tab/types.ts @@ -0,0 +1,6 @@ +import { Repo } from '@/types/repository' + +export interface TreeNodeData extends Repo { + isCurrentRepo?: boolean + origin?: boolean +} diff --git a/src/pages/repository/components/tabs/forks-tab/utils/prepareNodesAndEdgesFromRepo.ts b/src/pages/repository/components/tabs/forks-tab/utils/prepareNodesAndEdgesFromRepo.ts new file mode 100644 index 00000000..f4cc5e2a --- /dev/null +++ b/src/pages/repository/components/tabs/forks-tab/utils/prepareNodesAndEdgesFromRepo.ts @@ -0,0 +1,81 @@ +import { Edge, Node, Position } from '@xyflow/react' + +import { Repo } from '@/types/repository' + +import { TreeNodeData } from '../types' + +export function prepareNodesAndEdgesFromRepo(repos: Repo[], selectedRepoId: string) { + function buildTree(repo: Repo, reposMap: Map) { + const node: TreeNode = { + repo: repo, + children: [] + } + + if (repo.forks) { + for (const fork of Object.values(repo.forks)) { + if (reposMap.has(fork.id)) { + node.children.push(buildTree(reposMap.get(fork.id)!, reposMap)) + } + } + } + + return node + } + + const reposMap = new Map(repos.map((repo) => [repo.id, repo])) + const rootRepo = repos.find((repo) => !repo.parent) + const tree = rootRepo ? [buildTree(rootRepo, reposMap)] : [] + + const edges: Edge[] = [] + const nodes: Node[] = [] + const positionMap = new Map() + + function traverseTree(node: TreeNode, parentNode: Node | null, depth: number, siblingIndex: number) { + const x = parentNode ? parentNode.position.x + 500 : 0 + const y = parentNode ? parentNode.position.y + siblingIndex * 200 : 0 + + const newNode = { + id: node.repo.id, + type: 'custom', + sourcePosition: Position.Right, + targetPosition: Position.Left, + data: node.repo, + position: { x, y } + } + + if (!parentNode) { + ;(newNode.data as TreeNodeData).origin = true + ;(newNode.data as TreeNodeData).primary = true + } + + if (node.repo.id === selectedRepoId) { + ;(newNode.data as TreeNodeData).isCurrentRepo = true + } + + nodes.push(newNode) + positionMap.set(node.repo.id, { x, y }) + + if (parentNode) { + edges.push({ + id: `e${parentNode.id}-${node.repo.id}`, + source: parentNode.id, + target: node.repo.id + }) + } + + node.children.forEach((child, index) => { + traverseTree(child, newNode, depth + 1, index) + }) + } + + tree.forEach((rootNode, index) => { + traverseTree(rootNode, null, 0, index) + }) + + return { nodes, edges } +} + +type TreeNode = { + repo: Repo + children: TreeNode[] +} diff --git a/src/pages/repository/components/tabs/settings-tab/Contributors.tsx b/src/pages/repository/components/tabs/settings-tab/Contributors.tsx index 37724829..33026b09 100644 --- a/src/pages/repository/components/tabs/settings-tab/Contributors.tsx +++ b/src/pages/repository/components/tabs/settings-tab/Contributors.tsx @@ -230,7 +230,7 @@ export default function Contributors() { />
+
+ )} + + ) +} + +function LoadingBox() { + return ( +
+
+
+
+ ) +} diff --git a/src/pages/repository/components/tabs/settings-tab/Token.tsx b/src/pages/repository/components/tabs/settings-tab/Token.tsx new file mode 100644 index 00000000..68d0e3c2 --- /dev/null +++ b/src/pages/repository/components/tabs/settings-tab/Token.tsx @@ -0,0 +1,566 @@ +import { Listbox, ListboxButton, ListboxOption, ListboxOptions, Transition } from '@headlessui/react' +import { yupResolver } from '@hookform/resolvers/yup' +import clsx from 'clsx' +import { Fragment, useState } from 'react' +import React from 'react' +import { useForm } from 'react-hook-form' +import toast from 'react-hot-toast' +// import { FaPlus } from 'react-icons/fa' +import { HiChevronUpDown } from 'react-icons/hi2' +import { IoCheckmark } from 'react-icons/io5' +import * as yup from 'yup' + +import { Button } from '@/components/common/buttons' +import { isInvalidInput } from '@/helpers/isInvalidInput' +import { spawnBondingCurveProcess } from '@/lib/decentralize' +// import { fetchAllUserTokens } from '@/helpers/wallet/fetchAllTokens' +import { useGlobalStore } from '@/stores/globalStore' +import { Allocation, BondingCurve, RepoToken, type Token } from '@/types/repository' + +import LiquidityPoolTokenSetting from './LiquidityPoolTokenSetting' + +const CURVE_TYPES = ['Exponential-Like', 'Linear', 'Exponential', 'Quadratic'] + +const tokenSchema = yup + .object({ + tokenName: yup.string().required('Token name is required'), + tokenTicker: yup + .string() + .required('Ticker is required') + .matches(/^[A-Z]+$/, 'Must be uppercase letters and one word only'), + denomination: yup.string().required('Denomination is required').matches(/^\d+$/, 'Must be a number'), + totalSupply: yup.string().required('Total supply is required').matches(/^\d+$/, 'Must be a number'), + tokenImage: yup.string().required('Image is required'), + fundingGoal: yup.string().default('1500').matches(/^\d+$/, 'Must be a number') + // allocations: yup + // .array() + // .of( + // yup.object({ + // address: yup + // .string() + // .required('Wallet Address is required') + // .matches(/^[a-z0-9-_]{43}$/i, 'Must be a valid Arweave address'), + // percentage: yup.string().required('Percentage is required').matches(/^\d+$/, 'Must be a number') + // }) + // ) + // .required() + }) + .required() + +// const USDA_TST = { +// tokenName: ' MUSDAock', +// tokenTicker: 'TUSDA', +// processId: 'b87Jd4usKGyMjovbNeX4P3dcvkC4mrtBZ5HxW_ENtn4', +// denomination: '12', +// tokenImage: 'TPkPIvnvWuyd-hv8J1IAdUlb8aii00Z7vjwMBk_kp0M' +// } +const QAR = { + tokenName: 'Q Arweave', + tokenTicker: 'qAR', + // processId: 'b87Jd4usKGyMjovbNeX4P3dcvkC4mrtBZ5HxW_ENtn4', + processId: 'NG-0lVX882MG5nhARrSzyprEK6ejonHpdUmaaMPsHE8', + denomination: '12', + tokenImage: '26yDr08SuwvNQ4VnhAfV4IjJcOOlQ4tAQLc1ggrCPu0' +} +const RESERVE_TOKENS = [QAR] + +export default function Token() { + const [selectedCurveType] = useState(CURVE_TYPES[0]) + // const [isTokenListLoading, setIsTokenListLoading] = useState(true) + // const [tokenList, setTokenList] = useState<(typeof USDA_TST)[]>([USDA_TST]) + const [selectedToken, setSelectedToken] = useState(QAR) + const [isSubmitting, setIsSubmitting] = useState(false) + const [selectedRepo, isRepoOwner, saveRepoTokenDetails, saveRepoBondingCurveDetails] = useGlobalStore((state) => [ + state.repoCoreState.selectedRepo.repo, + state.repoCoreActions.isRepoOwner, + state.repoCoreActions.saveRepoTokenDetails, + state.repoCoreActions.saveRepoBondingCurveDetails + ]) + const { + register, + handleSubmit, + // control, + formState: { errors: tokenErrors } + } = useForm({ + resolver: yupResolver(tokenSchema), + mode: 'onChange', + defaultValues: { + tokenName: selectedRepo?.token?.tokenName || '', + tokenTicker: selectedRepo?.token?.tokenTicker || '', + denomination: selectedRepo?.token?.denomination || '', + totalSupply: selectedRepo?.token?.totalSupply || '', + tokenImage: selectedRepo?.token?.tokenImage || '', + fundingGoal: selectedRepo?.bondingCurve?.fundingGoal || '1500' + // allocations: selectedRepo?.token?.allocations || [] + } + }) + + // const { fields, append, remove } = useFieldArray({ + // name: 'allocations', + // control + // }) + + // React.useEffect(() => { + // fetchAllUserTokens().then((tokens) => { + // // setTokenList(tokens) + // setIsTokenListLoading(false) + // }) + // }, []) + + // React.useEffect(() => { + // if (fields.length === 0) { + // appendEmptyRecipient() + // } + // }, [fields]) + + React.useEffect(() => { + if (selectedRepo?.bondingCurve) { + setSelectedToken(selectedRepo.bondingCurve.reserveToken) + } + }, [selectedRepo]) + + // function appendEmptyRecipient() { + // append({ + // address: '', + // percentage: '' + // }) + // } + + // function hasAllKeysAndValues(obj: Record): boolean { + // const keys = ['tokenName', 'tokenTicker', 'denomination', 'tokenImage'] + // return keys.every( + // (key) => + // Object.prototype.hasOwnProperty.call(obj, key) && obj[key] !== null && obj[key] !== undefined && obj[key] !== '' + // ) + // } + + async function handleSubmitClick(data: yup.InferType) { + if (!selectedRepo) return + + if (selectedRepo.decentralized) { + toast.error('This repository is a decentralized repository. Cannot update token after this point.') + return + } + + setIsSubmitting(true) + + try { + const updatedFields = getUpdatedFields(selectedRepo.token || {}, data) + + if (!selectedRepo?.bondingCurve || !selectedRepo?.bondingCurve?.processId) { + // + } + const bondingCurve: BondingCurve = { + fundingGoal: data.fundingGoal || '1500', + reserveToken: selectedToken + } + if (!selectedRepo?.bondingCurve?.processId) { + const pid = await spawnBondingCurveProcess(data.tokenName) + + bondingCurve.processId = pid + } + + await saveRepoBondingCurveDetails(bondingCurve) + + if (Object.keys(updatedFields).length === 0) { + toast.success('Changes are in sync.') + return + } + + // if (updatedFields.allocations && !validateAllocations(updatedFields.allocations)) { + // toast.error('Allocations must not exceed 100%') + // return + // } + + await saveRepoTokenDetails(updatedFields) + } catch (error) { + toast.error('Failed to save token.') + } finally { + setIsSubmitting(false) + } + } + + // function handleDeleteAllocation(idx: number) { + // remove(idx) + // } + + function getUpdatedFields(originalData: Partial, updatedData: Partial): Partial { + const changes: Partial = {} + + Object.keys(updatedData).forEach((key: string) => { + const typedKey = key as keyof RepoToken + + if (!isInvalidInput(updatedData[typedKey], ['string', 'array'], true)) { + if (Array.isArray(updatedData[typedKey]) && typedKey === 'allocations') { + if (JSON.stringify(originalData[typedKey]) !== JSON.stringify(updatedData[typedKey])) { + changes[typedKey] = updatedData[typedKey] as Allocation[] + } + } else if (originalData[typedKey] !== updatedData[typedKey] && typedKey !== 'allocations') { + changes[typedKey] = updatedData[typedKey] + } + } + }) + + return changes + } + + // function validateAllocations(allocations: Allocation[]) { + // const percentage = allocations.reduce((acc, curr) => acc + parseInt(curr.percentage), 0) + // return percentage <= 100 + // } + + const repoOwner = isRepoOwner() + + return ( +
+
+

Token Settings

+
+
+ +
+
+ +
+ +
+ {tokenErrors.tokenName && ( +

{tokenErrors.tokenName?.message}

+ )} +
+
+ +
+ +
+ {tokenErrors.tokenTicker && ( +

{tokenErrors.tokenTicker?.message}

+ )} +
+
+
+
+ +
+ +
+ {tokenErrors.denomination && ( +

{tokenErrors.denomination?.message}

+ )} +
+
+ +
+ +
+ {tokenErrors.totalSupply && ( +

{tokenErrors.totalSupply?.message}

+ )} +
+
+

+ *20% of the maximum supply will be reserved for the bonding curve to create liquidity pool. +

+
+ +
+ +
+ {tokenErrors.tokenImage && ( +

{tokenErrors.tokenImage?.message}

+ )} +
+ {/*
+
+ +
+ {fields.map((field, idx) => { + return ( +
+
+

Allocation #{idx + 1}

+ {fields.length > 1 && !selectedRepo?.decentralized && repoOwner && ( + handleDeleteAllocation(idx)} + className="text-primary-700 text-sm font-medium !p-0 flex items-center cursor-pointer hover:underline" + > + Delete + + )} +
+
+
+
+ +
+ {tokenErrors.allocations && + tokenErrors.allocations[idx] && + tokenErrors.allocations![idx]?.address && ( +

+ {tokenErrors?.allocations![idx]?.address?.message} +

+ )} +
+
+
+ +
+ {tokenErrors.allocations && + tokenErrors.allocations[idx] && + tokenErrors.allocations![idx]?.percentage && ( +

+ {tokenErrors?.allocations![idx]?.percentage?.message} +

+ )} +
+
+ {idx === fields.length - 1 && ( +
+ +
+ )} +
+ ) + })} +
+
+
*/} + +
+
+ + + {selectedRepo?.liquidityPoolId && } + + {!selectedRepo?.liquidityPoolId && ( +
+
+ +
+
+ +
+ + {selectedCurveType} + + + + + + {CURVE_TYPES.map((curveType) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active ? 'bg-primary-100' : 'text-gray-900' + }` + } + value={curveType} + > + {({ selected }) => ( + <> + + {curveType} + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
+
+
+
+
+
+ +
+
+ +
+ + {selectedToken.tokenTicker} + + + + + + {RESERVE_TOKENS.map((token) => ( + + `relative cursor-default select-none py-2 pl-10 pr-4 ${ + active ? 'bg-primary-100' : 'text-gray-900' + }` + } + value={token} + > + {({ selected }) => ( + <> + + {token.tokenTicker} + + {selected ? ( + + + ) : null} + + )} + + ))} + + +
+
+
+
+
+ +
+ +
+ +
+ {tokenErrors.fundingGoal && ( +

{tokenErrors.fundingGoal?.message}

+ )} +
+
+ )} +
+
+
+ +
+
+
+ ) +} diff --git a/src/pages/repository/components/tabs/settings-tab/index.tsx b/src/pages/repository/components/tabs/settings-tab/index.tsx index 0730ec7c..1bba034e 100644 --- a/src/pages/repository/components/tabs/settings-tab/index.tsx +++ b/src/pages/repository/components/tabs/settings-tab/index.tsx @@ -1,13 +1,37 @@ import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react' +import React from 'react' +import { useNavigate, useParams } from 'react-router-dom' import { tabConfig } from './tabConfig' const activeClasses = 'text-gray-900 font-medium' export default function SettingsTab() { + const { id, settingsTabName } = useParams() + + const navigate = useNavigate() + function handleTabChangeEventTracking(idx: number) { + const tab = tabConfig[idx] + + const targetPath = tab.getPath(id!) + + navigate(targetPath) + } + const selectedIndex = React.useMemo(() => { + if (!settingsTabName) return 0 + const tabNames = tabConfig.map((tab) => tab.title.toLocaleLowerCase()) + const name = settingsTabName + return tabNames.indexOf(name) + }, [settingsTabName]) + return (
- + {tabConfig.map((tab) => ( diff --git a/src/pages/repository/components/tabs/settings-tab/tabConfig.ts b/src/pages/repository/components/tabs/settings-tab/tabConfig.ts index af4c8394..69cc55e0 100644 --- a/src/pages/repository/components/tabs/settings-tab/tabConfig.ts +++ b/src/pages/repository/components/tabs/settings-tab/tabConfig.ts @@ -3,26 +3,37 @@ import Deployments from './Deployments' import General from './General' import GithubSync from './GithubSync' import Insights from './Insights' +import Token from './Token' export const tabConfig = [ { title: 'General', - Component: General + Component: General, + getPath: (id: string, _?: string) => `/repository/${id}/settings/general` + }, + { + title: 'Token', + Component: Token, + getPath: (id: string, _?: string) => `/repository/${id}/settings/token` }, { title: 'Contributors', - Component: Contributors + Component: Contributors, + getPath: (id: string, _?: string) => `/repository/${id}/settings/contributors` }, { title: 'Insights', - Component: Insights + Component: Insights, + getPath: (id: string, _?: string) => `/repository/${id}/settings/insights` }, { title: 'Deployments', - Component: Deployments + Component: Deployments, + getPath: (id: string, _?: string) => `/repository/${id}/settings/deployments` }, { title: 'Github Sync', - Component: GithubSync + Component: GithubSync, + getPath: (id: string, _?: string) => `/repository/${id}/settings/github-sync` } ] diff --git a/src/pages/repository/config/rootTabConfig.ts b/src/pages/repository/config/rootTabConfig.ts index a65ea867..2968c450 100644 --- a/src/pages/repository/config/rootTabConfig.ts +++ b/src/pages/repository/config/rootTabConfig.ts @@ -1,5 +1,5 @@ import { BiCodeAlt } from 'react-icons/bi' -import { BsRocketTakeoff } from "react-icons/bs"; +import { BsRocketTakeoff } from 'react-icons/bs' import { FiGitBranch, FiGitCommit, FiGitPullRequest, FiSettings } from 'react-icons/fi' import { VscIssues } from 'react-icons/vsc' @@ -38,10 +38,10 @@ export const rootTabConfig = [ getPath: (id: string, _?: string) => `/repository/${id}/pulls` }, { - title: 'Forks', + title: 'Journey', Component: ForksTab, Icon: FiGitBranch, - getPath: (id: string, _?: string) => `/repository/${id}/forks` + getPath: (id: string, _?: string) => `/repository/${id}/journey` }, { title: 'Deployments', diff --git a/src/pages/repository/helpers/createConfetti.ts b/src/pages/repository/helpers/createConfetti.ts new file mode 100644 index 00000000..7de7f8a7 --- /dev/null +++ b/src/pages/repository/helpers/createConfetti.ts @@ -0,0 +1,29 @@ +import confetti from 'canvas-confetti' + +export function createConfetti() { + const duration = 5 * 1000 + const animationEnd = Date.now() + duration + const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 } + + const randomInRange = (min: number, max: number) => Math.random() * (max - min) + min + + const interval = window.setInterval(() => { + const timeLeft = animationEnd - Date.now() + + if (timeLeft <= 0) { + return clearInterval(interval) + } + + const particleCount = 50 * (timeLeft / duration) + confetti({ + ...defaults, + particleCount, + origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 } + }) + confetti({ + ...defaults, + particleCount, + origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 } + }) + }, 250) +} diff --git a/src/pages/repository/helpers/createTokenLua.ts b/src/pages/repository/helpers/createTokenLua.ts new file mode 100644 index 00000000..d724bcbd --- /dev/null +++ b/src/pages/repository/helpers/createTokenLua.ts @@ -0,0 +1,889 @@ +import { RepoToken } from '@/types/repository' + +export function createCurveBondedTokenLua(token: RepoToken, bondingCurveId: string): string { + let luaCode = ` +-- module: "src.utils.mod" +local function _loaded_mod_src_utils_mod() + local bint = require('.bint')(256) + + local utils = { + add = function(a, b) + return tostring(bint(a) + bint(b)) + end, + subtract = function(a, b) + return tostring(bint(a) - bint(b)) + end, + multiply = function(a, b) + return tostring(bint(a) * bint(b)) + end, + divide = function(a, b) + return tostring(bint.udiv(bint(a), bint(b))) + end, + toBalanceValue = function(a) + return tostring(bint(a)) + end, + toNumber = function(a) + return tonumber(a) + end, + toSubUnits = function(val, denom) + return bint(val) * bint.ipow(bint(10), bint(denom)) + end + } + + return utils + +end + +_G.package.loaded["src.utils.mod"] = _loaded_mod_src_utils_mod() + +-- module: "src.handlers.token" +local function _loaded_mod_src_handlers_token() + local utils = require "src.utils.mod" + + local mod = {} + + --- @type Denomination + Denomination = Denomination or ${+token.denomination} + --- @type Balances + Balances = Balances or { [ao.id] = utils.toBalanceValue(0) } + --- @type TotalSupply + TotalSupply = TotalSupply or utils.toBalanceValue(0) + --- @type Name + Name = Name or '${token.tokenName}' + --- @type Ticker + Ticker = Ticker or '${token.tokenTicker}' + --- @type Logo + Logo = Logo or '${token.tokenImage}' + --- @type MaxSupply + MaxSupply = MaxSupply or '${+token.totalSupply * 10 ** +token.denomination}' ; + --- @type BondingCurveProcess + BondingCurveProcess = BondingCurveProcess or '${bondingCurveId}'; + + -- Get token info + ---@type HandlerFunction + function mod.info(msg) + if msg.reply then + msg.reply({ + Action = 'Info-Response', + Name = Name, + Ticker = Ticker, + Logo = Logo, + Denomination = tostring(Denomination), + MaxSupply = MaxSupply, + TotalSupply = TotalSupply, + BondingCurveProcess = BondingCurveProcess, + }) + else + ao.send({ + Action = 'Info-Response', + Target = msg.From, + Name = Name, + Ticker = Ticker, + Logo = Logo, + Denomination = tostring(Denomination) + }) + end + end + + -- Get token total supply + ---@type HandlerFunction + function mod.totalSupply(msg) + assert(msg.From ~= ao.id, 'Cannot call Total-Supply from the same process!') + if msg.reply then + msg.reply({ + Action = 'Total-Supply-Response', + Data = TotalSupply, + Ticker = Ticker + }) + else + Send({ + Target = msg.From, + Action = 'Total-Supply-Response', + Data = TotalSupply, + Ticker = Ticker + }) + end + end + + -- Get token max supply + ---@type HandlerFunction + function mod.maxSupply(msg) + assert(msg.From ~= ao.id, 'Cannot call Max-Supply from the same process!') + + if msg.reply then + msg.reply({ + Action = 'Max-Supply-Response', + Data = MaxSupply, + Ticker = Ticker + }) + else + ao.send({ + Target = msg.From, + Action = 'Max-Supply-Response', + Data = MaxSupply, + Ticker = Ticker + }) + end + end + + return mod + +end + +_G.package.loaded["src.handlers.token"] = _loaded_mod_src_handlers_token() + +-- module: "src.libs.aolibs" +local function _loaded_mod_src_libs_aolibs() + -- These libs should exist in ao + + local mod = {} + + -- Define json + + local cjsonstatus, cjson = pcall(require, "cjson") + + if cjsonstatus then + mod.json = cjson + else + local jsonstatus, json = pcall(require, "json") + if not jsonstatus then + error("Library 'json' does not exist") + else + mod.json = json + end + end + + return mod +end + +_G.package.loaded["src.libs.aolibs"] = _loaded_mod_src_libs_aolibs() + +-- module: "src.handlers.balance" +local function _loaded_mod_src_handlers_balance() + local aolibs = require "src.libs.aolibs" + local json = aolibs.json + + local mod = {} + + -- Get target balance + ---@type HandlerFunction + function mod.balance(msg) + local bal = '0' + + -- If not Recipient is provided, then return the Senders balance + if (msg.Tags.Recipient) then + if (Balances[msg.Tags.Recipient]) then + bal = Balances[msg.Tags.Recipient] + end + elseif msg.Tags.Target and Balances[msg.Tags.Target] then + bal = Balances[msg.Tags.Target] + elseif Balances[msg.From] then + bal = Balances[msg.From] + end + if msg.reply then + msg.reply({ + Action = 'Balance-Response', + Balance = bal, + Ticker = Ticker, + Account = msg.Tags.Recipient or msg.From, + Data = bal + }) + else + ao.send({ + Action = 'Balance-Response', + Target = msg.From, + Balance = bal, + Ticker = Ticker, + Account = msg.Tags.Recipient or msg.From, + Data = bal + }) + end + end + + -- Get balances + ---@type HandlerFunction + function mod.balances(msg) + if msg.reply then + msg.reply({ Data = json.encode(Balances) }) + else + ao.send({ Target = msg.From, Data = json.encode(Balances) }) + end + end + + return mod + +end + +_G.package.loaded["src.handlers.balance"] = _loaded_mod_src_handlers_balance() + +-- module: "src.handlers.transfer" +local function _loaded_mod_src_handlers_transfer() + local bint = require('.bint')(256) + local utils = require "src.utils.mod" + + local mod = {} + + + function mod.transfer(msg) + assert(type(msg.Recipient) == 'string', 'Recipient is required!') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(bint.__lt(0, bint(msg.Quantity)), 'Quantity must be greater than 0') + + if not Balances[msg.From] then Balances[msg.From] = "0" end + if not Balances[msg.Recipient] then Balances[msg.Recipient] = "0" end + + if bint(msg.Quantity) <= bint(Balances[msg.From]) then + Balances[msg.From] = utils.subtract(Balances[msg.From], msg.Quantity) + Balances[msg.Recipient] = utils.add(Balances[msg.Recipient], msg.Quantity) + + --[[ + Only send the notifications to the Sender and Recipient + if the Cast tag is not set on the Transfer message + ]] + -- + if not msg.Cast then + -- Debit-Notice message template, that is sent to the Sender of the transfer + local debitNotice = { + Action = 'Debit-Notice', + Recipient = msg.Recipient, + Quantity = msg.Quantity, + Data = Colors.gray .. + "You transferred " .. + Colors.blue .. msg.Quantity .. Colors.gray .. " to " .. Colors.green .. msg.Recipient .. Colors.reset + } + -- Credit-Notice message template, that is sent to the Recipient of the transfer + local creditNotice = { + Target = msg.Recipient, + Action = 'Credit-Notice', + Sender = msg.From, + Quantity = msg.Quantity, + Data = Colors.gray .. + "You received " .. + Colors.blue .. msg.Quantity .. Colors.gray .. " from " .. Colors.green .. msg.From .. Colors.reset + } + + -- Add forwarded tags to the credit and debit notice messages + for tagName, tagValue in pairs(msg) do + -- Tags beginning with "X-" are forwarded + if string.sub(tagName, 1, 2) == "X-" then + debitNotice[tagName] = tagValue + creditNotice[tagName] = tagValue + end + end + + -- Send Debit-Notice and Credit-Notice + if msg.reply then + msg.reply(debitNotice) + else + debitNotice.Target = msg.From + Send(debitNotice) + end + Send(creditNotice) + end + else + if msg.reply then + msg.reply({ + Action = 'Transfer-Error', + ['Message-Id'] = msg.Id, + Error = 'Insufficient Balance!' + }) + else + Send({ + Target = msg.From, + Action = 'Transfer-Error', + ['Message-Id'] = msg.Id, + Error = 'Insufficient Balance!' + }) + end + end + end + + return mod + +end + +_G.package.loaded["src.handlers.transfer"] = _loaded_mod_src_handlers_transfer() + +-- module: "arweave.types.type" +local function _loaded_mod_arweave_types_type() + ---@class Type + local Type = { + -- custom name for the defined type + ---@type string|nil + name = nil, + -- list of assertions to perform on any given value + ---@type { message: string, validate: fun(val: any): boolean }[] + conditions = nil + } + + -- Execute an assertion for a given value + ---@param val any Value to assert for + ---@param message string? Optional message to throw + ---@param no_error boolean? Optionally disable error throwing (will return boolean) + function Type:assert(val, message, no_error) + for _, condition in ipairs(self.conditions) do + if not condition.validate(val) then + if no_error then + return false + end + self:error(message or condition.message) + end + end + + if no_error then + return true + end + end + + -- Add a custom condition/assertion to assert for + ---@param message string Error message for the assertion + ---@param assertion fun(val: any): boolean Custom assertion function that is asserted with the provided value + function Type:custom(message, assertion) + -- condition to add + local condition = { + message = message, + validate = assertion + } + + -- new instance if there are no conditions yet + if self.conditions == nil then + local instance = { + conditions = {} + } + + table.insert(instance.conditions, condition) + setmetatable(instance, self) + self.__index = self + + return instance + end + + table.insert(self.conditions, condition) + return self + end + + -- Add an assertion for built in types + ---@param t "nil"|"number"|"string"|"boolean"|"table"|"function"|"thread"|"userdata" Type to assert for + ---@param message string? Optional assertion error message + function Type:type(t, message) + return self:custom(message or ("Not of type (" .. t .. ")"), function(val) + return type(val) == t + end) + end + + -- Type must be userdata + ---@param message string? Optional assertion error message + function Type:userdata(message) + return self:type("userdata", message) + end + + -- Type must be thread + ---@param message string? Optional assertion error message + function Type:thread(message) + return self:type("thread", message) + end + + -- Type must be table + ---@param message string? Optional assertion error message + function Type:table(message) + return self:type("table", message) + end + + -- Table's keys must be of type t + ---@param t Type Type to assert the keys for + ---@param message string? Optional assertion error message + function Type:keys(t, message) + return self:custom(message or "Invalid table keys", function(val) + if type(val) ~= "table" then + return false + end + + for key, _ in pairs(val) do + -- check if the assertion throws any errors + local success = pcall(function() + return t:assert(key) + end) + + if not success then + return false + end + end + + return true + end) + end + + -- Type must be array + ---@param message string? Optional assertion error message + function Type:array(message) + return self:table():keys(Type:number(), message) + end + + -- Table's values must be of type t + ---@param t Type Type to assert the values for + ---@param message string? Optional assertion error message + function Type:values(t, message) + return self:custom(message or "Invalid table values", function(val) + if type(val) ~= "table" then + return false + end + + for _, v in pairs(val) do + -- check if the assertion throws any errors + local success = pcall(function() + return t:assert(v) + end) + + if not success then + return false + end + end + + return true + end) + end + + -- Type must be boolean + ---@param message string? Optional assertion error message + function Type:boolean(message) + return self:type("boolean", message) + end + + -- Type must be function + ---@param message string? Optional assertion error message + function Type:_function(message) + return self:type("function", message) + end + + -- Type must be nil + ---@param message string? Optional assertion error message + function Type:_nil(message) + return self:type("nil", message) + end + + -- Value must be the same + ---@param val any The value the assertion must be made with + ---@param message string? Optional assertion error message + function Type:is(val, message) + return self:custom(message + or "Value did not match expected value (Type:is(expected))", + function(v) + return v == val + end) + end + + -- Type must be string + ---@param message string? Optional assertion error message + function Type:string(message) + return self:type("string", message) + end + + -- String type must match pattern + ---@param pattern string Pattern to match + ---@param message string? Optional assertion error message + function Type:match(pattern, message) + return self:custom(message + or ("String did not match pattern"), + function(val) + return string.match(val, pattern) ~= nil + end) + end + + -- String type must be of defined length + ---@param len number Required length + ---@param match_type? "less"|"greater" String length should be "less" than or "greater" than the defined length. Leave empty for exact match. + ---@param message string? Optional assertion error message + function Type:length(len, match_type, message) + local match_msgs = { + less = "String length is not less than " .. len, + greater = "String length is not greater than " .. len, + default = "String is not of length " .. len + } + + return self:custom(message or (match_msgs[match_type] or match_msgs.default), + function(val) + local strlen = string.len(val) + + -- validate length + if match_type == "less" then + return strlen < len + elseif match_type == "greater" then + return strlen > len + end + + return strlen == len + end) + end + + -- Type must be a number + ---@param message string? Optional assertion error message + function Type:number(message) + return self:type("number", message) + end + + -- Number must be an integer (chain after "number()") + ---@param message string? Optional assertion error message + function Type:integer(message) + return self:custom(message or "Number is not an integer", function(val) + return val % 1 == 0 + end) + end + + -- Number must be even (chain after "number()") + ---@param message string? Optional assertion error message + function Type:even(message) + return self:custom(message or "Number is not even", function(val) + return val % 2 == 0 + end) + end + + -- Number must be odd (chain after "number()") + ---@param message string? Optional assertion error message + function Type:odd(message) + return self:custom(message or "Number is not odd", function(val) + return val % 2 == 1 + end) + end + + -- Number must be less than the number "n" (chain after "number()") + ---@param n number Number to compare with + ---@param message string? Optional assertion error message + function Type:less_than(n, message) + return self:custom(message or ("Number is not less than " .. n), function(val) + return val < n + end) + end + + -- Number must be greater than the number "n" (chain after "number()") + ---@param n number Number to compare with + ---@param message string? Optional assertion error message + function Type:greater_than(n, message) + return self:custom(message or ("Number is not greater than" .. n), + function(val) + return val > n + end) + end + + -- Make a type optional (allow them to be nil apart from the required type) + ---@param t Type Type to assert for if the value is not nil + ---@param message string? Optional assertion error message + function Type:optional(t, message) + return self:custom(message or "Optional type did not match", function(val) + if val == nil then + return true + end + + t:assert(val) + return true + end) + end + + -- Table must be of object + ---@param obj { [any]: Type } + ---@param strict? boolean Only allow the defined keys from the object, throw error on other keys (false by default) + ---@param message string? Optional assertion error message + function Type:object(obj, strict, message) + if type(obj) ~= "table" then + self:error( + "Invalid object structure provided for object assertion (has to be a table):" + .. tostring(obj)) + end + + return self:custom(message + or ("Not of defined object (" .. tostring(obj) .. ")"), + function(val) + if type(val) ~= "table" then + return false + end + + -- for each value, validate + for key, assertion in pairs(obj) do + if val[key] == nil then + return false + end + + -- check if the assertion throws any errors + local success = pcall(function() + return assertion:assert(val[key]) + end) + + if not success then + return false + end + end + + -- in strict mode, we do not allow any other keys + if strict then + for key, _ in pairs(val) do + if obj[key] == nil then + return false + end + end + end + + return true + end) + end + + -- Type has to be either one of the defined assertions + ---@param ... Type Type(s) to assert for + function Type:either(...) + ---@type Type[] + local assertions = { + ... + } + + return self:custom("Neither types matched defined in (Type:either(...))", + function(val) + for _, assertion in ipairs(assertions) do + if pcall(function() + return assertion:assert(val) + end) then + return true + end + end + + return false + end) + end + + -- Type cannot be the defined assertion (tip: for multiple negated assertions, use Type:either(...)) + ---@param t Type Type to NOT assert for + ---@param message string? Optional assertion error message + function Type:is_not(t, message) + return self:custom(message + or "Value incorrectly matched with the assertion provided (Type:is_not())", + function(val) + local success = pcall(function() + return t:assert(val) + end) + + return not success + end) + end + + -- Set the name of the custom type + -- This will be used with error logs + ---@param name string Name of the type definition + function Type:set_name(name) + self.name = name + return self + end + + -- Throw an error + ---@param message any Message to log + ---@private + function Type:error(message) + error("[Type " .. (self.name or tostring(self.__index)) .. "] " + .. tostring(message)) + end + + return Type + +end + +_G.package.loaded["arweave.types.type"] = _loaded_mod_arweave_types_type() + +-- module: "src.utils.assertions" +local function _loaded_mod_src_utils_assertions() + local Type = require "arweave.types.type" + + local mod = {} + + ---Assert value is an Arweave address + ---@param name string + ---@param value string + mod.isAddress = function(name, value) + Type + ` + luaCode += ':string("Invalid type for `" .. name .. "`. Expected a string for Arweave address.")\n' + luaCode += ':length(43, nil, "Incorrect length for Arweave address `" .. name .. "`. Must be exactly 43 characters long.")\n' + luaCode += ':match("[a-zA-Z0-9-_]+",\n' + luaCode +='"Invalid characters in Arweave address `" ..\n' + luaCode +='name .. "`. Only alphanumeric characters, dashes, and underscores are allowed.")\n' + luaCode +=':assert(value)\n' + luaCode += `end + + ---Assert value is an UUID + ---@param name string + ---@param value string + mod.isUuid = function(name, value) + Type + ` + luaCode += ':string("Invalid type for `" .. name .. "`. Expected a string for UUID.")' + luaCode += ':match("^[0-9a-fA-F]%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$",' + luaCode += ' "Invalid UUID format for `" ..' + luaCode +=' name .. "`. A valid UUID should follow the 8-4-4-4-12 hexadecimal format.")' + luaCode +=' :assert(value)' + luaCode += `end + + mod.Array = Type:array("Invalid type (must be array)") + + -- string assertion + mod.String = Type:string("Invalid type (must be a string)") + + -- Assert not empty string + ---@param value any Value to assert for + ---@param message string? Optional message to throw + ---@param len number Required length + ---@param match_type? "less"|"greater" String length should be "less" than or "greater" than the defined length. Leave empty for exact match. + ---@param len_message string? Optional assertion error message for length + mod.assertNotEmptyString = function(value, message, len, match_type, len_message) + Type:string(message):length(len, match_type, len_message):assert(value) + end + + -- number assertion + mod.Integer = Type:number():integer("Invalid type (must be a integer)") + -- number assertion + mod.Number = Type:number("Invalid type (must be a number)") + + -- repo name assertion + mod.RepoName = Type + :string("Invalid type for Repository name (must be a string)") + :match("^[a-zA-Z0-9._-]+$", + "The repository name can only contain ASCII letters, digits, and the characters ., -, and _") + + return mod + +end + +_G.package.loaded["src.utils.assertions"] = _loaded_mod_src_utils_assertions() + +-- module: "src.handlers.mint" +local function _loaded_mod_src_handlers_mint() + local bint = require('.bint')(256) + local utils = require "src.utils.mod" + local assertions = require "src.utils.assertions" + local mod = {} + + function mod.mint(msg) + assert(msg.From == BondingCurveProcess, 'Only the bonding curve process can mint!') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + assert(bint.__lt(bint(0), bint(msg.Quantity)), 'Quantity must be greater than zero!') + + -- Check if minting would exceed max supply + local newTotalSupply = utils.add(TotalSupply, msg.Quantity) + + if bint.__lt(bint(MaxSupply), bint(newTotalSupply)) then + msg.reply({ + Action = 'Mint-Error', + Error = 'Minting would exceed max supply!' + }) + + return + end + + -- Calculate required reserve amount + local recipient = msg.Tags.Recipient or msg.From + + assertions.isAddress("Recipient", recipient) + + -- Update balances + if not Balances[recipient] then Balances[recipient] = "0" end + + Balances[recipient] = utils.add(Balances[recipient], msg.Quantity) + TotalSupply = utils.add(TotalSupply, msg.Quantity) + + if msg.reply then + msg.reply({ + Action = 'Mint-Response', + Data = "Successfully minted " .. msg.Quantity + }) + else + ao.send({ + Action = 'Mint-Response', + Target = msg.From, + Data = "Successfully minted " .. msg.Quantity + }) + end + end + + return mod + +end + +_G.package.loaded["src.handlers.mint"] = _loaded_mod_src_handlers_mint() + +-- module: "src.handlers.burn" +local function _loaded_mod_src_handlers_burn() + local bint = require('.bint')(256) + local utils = require "src.utils.mod" + local assertions = require "src.utils.assertions" + local mod = {} + + function mod.burn(msg) + assert(msg.From == BondingCurveProcess, 'Only the bonding curve process can burn!') + assert(type(msg.Quantity) == 'string', 'Quantity is required!') + + local user = msg.Tags.Recipient + assertions.isAddress("Recipient", user) + + if bint.__lt(bint(Balances[user]), bint(msg.Quantity)) then + msg.reply({ + Action = 'Burn-Error', + Error = 'Quantity must be less than or equal to the current balance' + }) + + return + end + + -- Update balances + Balances[user] = utils.subtract(Balances[user], msg.Quantity) + TotalSupply = utils.subtract(TotalSupply, msg.Quantity) + + + + if msg.reply then + msg.reply({ + Action = 'Burn-Response', + Data = "Successfully burned " .. msg.Quantity + }) + else + ao.send({ + Action = 'Burn-Response', + + Target = msg.From, + Data = "Successfully burned " .. msg.Quantity + }) + end + end + + return mod + +end + +_G.package.loaded["src.handlers.burn"] = _loaded_mod_src_handlers_burn() + +local token = require "src.handlers.token" +local balance = require "src.handlers.balance" +local transfer = require "src.handlers.transfer" +local mint = require "src.handlers.mint" +local burn = require "src.handlers.burn" + +-- Info +Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), token.info) + +-- Total Supply +Handlers.add('Total-Supply', Handlers.utils.hasMatchingTag('Action', "Total-Supply"), token.totalSupply) + +-- Max Supply +Handlers.add('Max-Supply', Handlers.utils.hasMatchingTag('Action', "Max-Supply"), token.maxSupply) + +-- Balance +Handlers.add('Balance', Handlers.utils.hasMatchingTag('Action', 'Balance'), balance.balance) + +-- Balances +Handlers.add('Balances', Handlers.utils.hasMatchingTag('Action', 'Balances'), balance.balances) + +-- Transfer +Handlers.add('Transfer', Handlers.utils.hasMatchingTag('Action', 'Transfer'), transfer.transfer) + +-- Mint +Handlers.add('Mint', Handlers.utils.hasMatchingTag('Action', 'Mint'), mint.mint) + +-- Burn +Handlers.add('Burn', Handlers.utils.hasMatchingTag('Action', 'Burn'), burn.burn) +` + + return luaCode +} diff --git a/src/pages/repository/helpers/roundToSigFigures.ts b/src/pages/repository/helpers/roundToSigFigures.ts new file mode 100644 index 00000000..b33f009c --- /dev/null +++ b/src/pages/repository/helpers/roundToSigFigures.ts @@ -0,0 +1,9 @@ +export function roundToSignificantFigures(num: number, sig: number) { + if (num === 0) { + return 0 + } + const d = Math.ceil(Math.log10(Math.abs(num))) + const power = sig - d + const mult = Math.pow(10, power) + return Math.floor(num * mult + 0.5) / mult +} diff --git a/src/pages/repository/hooks/useCommit.ts b/src/pages/repository/hooks/useCommit.ts index 255bf754..311f95a6 100644 --- a/src/pages/repository/hooks/useCommit.ts +++ b/src/pages/repository/hooks/useCommit.ts @@ -4,7 +4,6 @@ import toast from 'react-hot-toast' import { trackGoogleAnalyticsEvent } from '@/helpers/google-analytics' import { withAsync } from '@/helpers/withAsync' -import { postUpdatedRepo } from '@/lib/git' import { getCurrentBranch } from '@/lib/git/branch' import { addFilesForCommit, @@ -15,6 +14,7 @@ import { stageFilesForCommit } from '@/lib/git/commit' import { fsWithName } from '@/lib/git/helpers/fsWithName' +import taskQueueSingleton from '@/lib/queue/TaskQueue' import { postCommitStatDataTxToArweave } from '@/lib/user' import { useGlobalStore } from '@/stores/globalStore' import { CommitResult } from '@/types/commit' @@ -31,8 +31,7 @@ type AddFilesOptions = { } export default function useCommit() { - const [selectedRepo, repoCommitsG, setRepoCommitsG, triggerGithubSync] = useGlobalStore((state) => [ - state.repoCoreState.selectedRepo, + const [repoCommitsG, setRepoCommitsG, triggerGithubSync] = useGlobalStore((state) => [ state.repoCoreState.git.commits, state.repoCoreActions.git.setCommits, state.repoCoreActions.triggerGithubSync @@ -85,11 +84,7 @@ export default function useCommit() { if (commitError || !commitSHA) throw trackAndThrowError('Failed to commit files', name, id) - const isPrivate = selectedRepo.repo?.private || false - const privateStateTxId = selectedRepo.repo?.privateStateTxId - const { error, response } = await withAsync(() => - postUpdatedRepo({ fs, dir, owner, id, isPrivate, privateStateTxId }) - ) + const { error, response } = await withAsync(() => taskQueueSingleton.execute(id)) if (error) throw trackAndThrowError('Failed to update repository', name, id) diff --git a/src/stores/pull-request/actions/index.ts b/src/stores/pull-request/actions/index.ts index 1e176f5f..02b73eaf 100644 --- a/src/stores/pull-request/actions/index.ts +++ b/src/stores/pull-request/actions/index.ts @@ -1,4 +1,4 @@ -import git from 'isomorphic-git' +import git from '@protocol.land/isomorphic-git' import { withAsync } from '@/helpers/withAsync' import { checkoutBranch, createNewBranch, getCurrentBranch } from '@/lib/git/branch' diff --git a/src/stores/pull-request/index.ts b/src/stores/pull-request/index.ts index 35c9e576..ba6e86f1 100644 --- a/src/stores/pull-request/index.ts +++ b/src/stores/pull-request/index.ts @@ -52,10 +52,8 @@ const createPullRequestSlice: StateCreator { - const { error, response } = await withAsync(() => - loadRepository(id, dataTxId, uploadStrategy, privateStateTxId) - ) + const loadRepoWithStatus = async ({ id }: Repo) => { + const { error, response } = await withAsync(() => loadRepository(id)) return !error && response && response.success ? { success: true } : { success: false } } diff --git a/src/stores/repository-core/actions/git.ts b/src/stores/repository-core/actions/git.ts index 1d235b8e..04a90a5e 100644 --- a/src/stores/repository-core/actions/git.ts +++ b/src/stores/repository-core/actions/git.ts @@ -1,11 +1,15 @@ import Arweave from 'arweave' import toast from 'react-hot-toast' -import { getArrayBufSize } from '@/helpers/getArrayBufSize' +import { getRepoSize } from '@/helpers/getArrayBufSize' import { waitFor } from '@/helpers/waitFor' import { getActivePublicKey } from '@/helpers/wallet/getPublicKey' import { withAsync } from '@/helpers/withAsync' -import { importRepoFromBlob, unmountRepoFromBrowser } from '@/lib/git' +import { ArFSSingleton } from '@/lib/arfs/arfsSingleton' +import arfsSingletonMap from '@/lib/arfs/arfsSingletonMap' +import { getArFS } from '@/lib/arfs/getArFS' +import { getBifrost } from '@/lib/arfs/getBifrost' +import { unmountRepoFromBrowser } from '@/lib/git' import { getAllCommits } from '@/lib/git/commit' import { fsWithName } from '@/lib/git/helpers/fsWithName' import { getOidFromRef, readFileFromOid, readFilesFromOid } from '@/lib/git/helpers/oid' @@ -57,27 +61,38 @@ export async function saveRepository(id: string, name: string) { document.body.removeChild(downloadLink) } -export async function loadRepository(id: string, dataTxId: string, uploadStrategy: string, privateStateTxId?: string) { - await unmountRepository(id) +export async function loadRepository(id: string) { + const arfs = getArFS() - const gatewayUrl = uploadStrategy === 'ARSEEDING' ? 'https://arseed.web3infra.dev' : 'https://arweave.net' - const response = await fetch(`${gatewayUrl}/${dataTxId}`) - let repoArrayBuf = await response.arrayBuffer() + const drive = await arfs.drive.get(id) - if (privateStateTxId) { - repoArrayBuf = await decryptRepo(repoArrayBuf, privateStateTxId) - } + const bifrost = getBifrost(drive!, arfs) + await bifrost.buildDriveState() + await waitFor(500) - const fs = fsWithName(id) - const dir = `/${id}` + await bifrost.syncDrive() + + const arfsSingleton = new ArFSSingleton() + + arfsSingleton.setArFS(arfs) + arfsSingleton.setDrive(drive!) + arfsSingleton.setBifrost(bifrost) - const repoSize = getArrayBufSize(repoArrayBuf) + arfsSingletonMap.setArFSSingleton(id, arfsSingleton) - const success = await importRepoFromBlob(fs, dir, new Blob([repoArrayBuf])) + let repoSize = 0 - await waitFor(1000) + if (bifrost.driveState) { + for (const entry in bifrost.driveState) { + const entity = bifrost.driveState[entry] + + if (entity.entityType === 'folder' || !entity.size) continue + + repoSize += entity.size + } + } - return { success, repoSize } + return { success: true, repoSize: getRepoSize(repoSize) } } export async function renameRepoDir(id: string, currentName: string, newName: string) { diff --git a/src/stores/repository-core/actions/repoMeta.ts b/src/stores/repository-core/actions/repoMeta.ts index dcc5e92f..f5200831 100644 --- a/src/stores/repository-core/actions/repoMeta.ts +++ b/src/stores/repository-core/actions/repoMeta.ts @@ -1,10 +1,12 @@ -import { dryrun } from '@permaweb/aoconnect' +import { dryrun, result } from '@permaweb/aoconnect' +import { Tag } from 'arweave/web/lib/transaction' import { AOS_PROCESS_ID } from '@/helpers/constants' import { getTags } from '@/helpers/getTags' import { getRepo, sendMessage } from '@/lib/contract' +import { pollForTxBeingAvailable } from '@/lib/decentralize' import { useGlobalStore } from '@/stores/globalStore' -import { Repo } from '@/types/repository' +import { BondingCurve, Repo, RepoToken } from '@/types/repository' // Repo Meta export const getRepositoryMetaFromContract = async (id: string): Promise<{ result: Repo }> => { @@ -90,3 +92,112 @@ export const handleCancelContributorInvite = async (id: string, contributor: str return repo.contributorInvites } + +export const handleSaveRepoToken = async (id: string, repoToken: Partial, address: string) => { + await sendMessage({ + tags: getTags({ + Action: 'Save-Token-Settings', + Id: id + }), + data: JSON.stringify(repoToken) + }) + + const { Messages } = await dryrun({ + process: AOS_PROCESS_ID, + tags: getTags({ Action: 'Get-Repo-Token-Details', Id: id }), + Owner: address + }) + + const repoTokenDetails = JSON.parse(Messages[0].Data)?.result as RepoToken + + return repoTokenDetails +} + +export const handleSaveRepoBondingCurve = async (id: string, bondingCurve: BondingCurve, address: string) => { + await sendMessage({ + tags: getTags({ + Action: 'Save-Bonding-Curve-Settings', + Id: id + }), + data: JSON.stringify(bondingCurve) + }) + + const { Messages } = await dryrun({ + process: AOS_PROCESS_ID, + tags: getTags({ Action: 'Get-Repo-Bonding-Curve-Details', Id: id }), + Owner: address + }) + + const repoBondingCurveDetails = JSON.parse(Messages[0].Data)?.result as BondingCurve + + return repoBondingCurveDetails +} + +export const handleSaveBondingCurveId = async (id: string, bondingCurveId: string) => { + const msgId = await sendMessage({ + tags: getTags({ + Action: 'Save-Repo-Bonding-Curve-Id', + Id: id, + 'Bonding-Curve-Id': bondingCurveId + }), + pid: AOS_PROCESS_ID + }) + + await pollForTxBeingAvailable({ txId: msgId }) + + const { Messages } = await result({ + message: msgId, + process: AOS_PROCESS_ID + }) + + if (!Messages[0]) { + throw new Error('Failed to save bonding curve id') + } + + const action = Messages[0].Tags.find( + (tag: Tag) => tag.name === 'Action' && tag.value === 'Repo-Bonding-Curve-Id-Updated' + ) + + if (!action) { + throw new Error('Failed to save bonding curve id') + } +} + +export const handleSaveLiquidityPoolId = async (id: string, liquidityPoolId: string) => { + const msgId = await sendMessage({ + tags: getTags({ + Action: 'Save-Repo-Liquidity-Pool-Id', + Id: id, + 'Liquidity-Pool-Id': liquidityPoolId + }), + pid: AOS_PROCESS_ID + }) + + await pollForTxBeingAvailable({ txId: msgId }) + + const { Messages } = await result({ + message: msgId, + process: AOS_PROCESS_ID + }) + + if (!Messages[0]) { + throw new Error('Failed to save liquidity pool id') + } + + const action = Messages[0].Tags.find( + (tag: Tag) => tag.name === 'Action' && tag.value === 'Repo-Token-Liquidity-Pool-Id-Updated' + ) + + if (!action) { + throw new Error('Failed to save liquidity pool id') + } +} + +export const fetchRepoHierarchy = async (id: string) => { + const { Messages } = await dryrun({ + process: AOS_PROCESS_ID, + tags: getTags({ Action: 'Get-Repo-Hierarchy', Id: id }) + }) + + return JSON.parse(Messages[0].Data) +} diff --git a/src/stores/repository-core/index.ts b/src/stores/repository-core/index.ts index 4e00220d..4d08a6e8 100644 --- a/src/stores/repository-core/index.ts +++ b/src/stores/repository-core/index.ts @@ -1,3 +1,4 @@ +import toast from 'react-hot-toast' import { StateCreator } from 'zustand' import { trackGoogleAnalyticsEvent } from '@/helpers/google-analytics' @@ -15,6 +16,7 @@ import { updateRepoDescription, updateRepoName } from '@/lib/git' +import { prepareNodesAndEdgesFromRepo } from '@/pages/repository/components/tabs/forks-tab/utils/prepareNodesAndEdgesFromRepo' import { useRepoHeaderStore } from '@/pages/repository/store/repoHeader' import { Deployment, Domain, GithubSync } from '@/types/repository' @@ -23,6 +25,7 @@ import { CombinedSlices } from '../types' import { countCommits, decryptPAT, + fetchRepoHierarchy, getFileContentFromOid, getFilesFromOid, getOidOfHeadRef, @@ -30,8 +33,11 @@ import { handleAcceptContributor, handleCancelContributorInvite, handleRejectContributor, + handleSaveBondingCurveId, + handleSaveLiquidityPoolId, + handleSaveRepoBondingCurve, + handleSaveRepoToken, loadRepository, - renameRepoDir, saveRepository } from './actions' import { RepoCoreSlice, RepoCoreState } from './types' @@ -41,6 +47,10 @@ const initialRepoCoreState: RepoCoreState = { status: 'IDLE', error: null, repo: null, + repoHierarchy: { + edges: [], + nodes: [] + }, statistics: { commits: [], pullRequests: [], @@ -83,6 +93,142 @@ const createRepoCoreSlice: StateCreator { + const repo = get().repoCoreState.selectedRepo.repo + const userAddress = get().authState.address + + if (!repo || !userAddress) { + toast.error('Not authorized to toggle decentralization.') + return + } + + const { error, response } = await withAsync(() => fetchRepoHierarchy(repo.id)) + if (error || !response.result) { + toast.error('Failed to fetch repo hierarchy.') + return + } + + const hierarchy = prepareNodesAndEdgesFromRepo(response.result, repo.id) + + set((state) => { + state.repoCoreState.selectedRepo.repoHierarchy = hierarchy + }) + }, + setRepoDecentralized: () => { + const repo = get().repoCoreState.selectedRepo.repo + const userAddress = get().authState.address + + if (!repo || !userAddress) { + toast.error('Not authorized to toggle decentralization.') + return + } + if (repo.decentralized) { + toast.error('Repository is already decentralized') + return + } + + set((state) => { + state.repoCoreState.selectedRepo.repo!.decentralized = true + }) + }, + saveRepoTokenDetails: async (token) => { + const repo = get().repoCoreState.selectedRepo.repo + const userAddress = get().authState.address + + if (!repo || !userAddress) { + toast.error('Not authorized to update token.') + return + } + + const { error, response } = await withAsync(() => handleSaveRepoToken(repo.id, token, userAddress)) + + if (error) { + toast.error('Failed to save token.') + return + } + + if (response) { + set((state) => { + state.repoCoreState.selectedRepo.repo!.token = response + }) + toast.success('Token saved.') + } + }, + saveRepoBondingCurveDetails: async (bondingCurve) => { + const repo = get().repoCoreState.selectedRepo.repo + const userAddress = get().authState.address + + if (!repo || !userAddress) { + toast.error('Not authorized to update token.') + return + } + + const { error, response } = await withAsync(() => handleSaveRepoBondingCurve(repo.id, bondingCurve, userAddress)) + + if (error) { + toast.error('Failed to save bonding curve.') + return + } + + if (response) { + set((state) => { + state.repoCoreState.selectedRepo.repo!.bondingCurve = response + }) + toast.success('Bonding curve saved.') + } + }, + saveBondingCurveId: async (bondingCurveId) => { + const repo = get().repoCoreState.selectedRepo.repo + const userAddress = get().authState.address + + if (!repo || !userAddress) { + toast.error('Not authorized to save bonding curve id.') + return + } + + const { error } = await withAsync(() => handleSaveBondingCurveId(repo.id, bondingCurveId)) + + if (error) { + toast.error('Failed to save bonding curve id.') + return + } + }, + saveLiquidityPoolId: async (liquidityPoolId) => { + const repo = get().repoCoreState.selectedRepo.repo + const userAddress = get().authState.address + + if (!repo || !userAddress) { + toast.error('Not authorized to save liquidity pool id.') + return + } + + const { error } = await withAsync(() => handleSaveLiquidityPoolId(repo.id, liquidityPoolId)) + + if (error) { + toast.error('Failed to save liquidity pool id.') + return + } + }, + setRepoTokenProcessId: (processId) => { + const repo = get().repoCoreState.selectedRepo.repo + const userAddress = get().authState.address + + if (!repo || !userAddress) { + toast.error('Not authorized to update token.') + return + } + + const token = repo.token + + if (!token) { + toast.error('Token not found.') + return + } + + set((state) => { + state.repoCoreState.selectedRepo.repo!.token!.processId = processId + }) + }, isRepoOwner: () => { const repo = get().repoCoreState.selectedRepo.repo const userAddress = get().authState.address @@ -101,6 +247,12 @@ const createRepoCoreSlice: StateCreator getRepositoryMetaFromContract(parent!) @@ -646,22 +788,16 @@ const createRepoCoreSlice: StateCreator - loadRepository(repoId, dataTxId, uploadStrategy, privateStateTxId) - ) + const { error: repoFetchError, response: repoFetchResponse } = await withAsync(() => loadRepository(repoId)) - if (fork && parentRepoId && repoId !== parentRepoId) { - const renamed = await renameRepoDir(repoId, parentRepoId, repoId) + // if (fork && parentRepoId && repoId !== parentRepoId) { + // const renamed = await renameRepoDir(repoId, parentRepoId, repoId) - if (!renamed) throw new Error('Error loading the repository.') - } + // if (!renamed) throw new Error('Error loading the repository.') + // } // Always checkout default master branch if available if (!repoFetchError && repoFetchResponse && repoFetchResponse.success) { @@ -692,6 +828,8 @@ const createRepoCoreSlice: StateCreator { state.repoCoreState.selectedRepo.status = 'SUCCESS' }) @@ -708,9 +846,7 @@ const createRepoCoreSlice: StateCreator - loadRepository(repo.id, repo.dataTxId, repo.uploadStrategy, repo.privateStateTxId) - ) + const { error: repoFetchError, response: repoFetchResponse } = await withAsync(() => loadRepository(repo.id)) if (repoFetchError) { set((state) => { @@ -742,12 +878,7 @@ const createRepoCoreSlice: StateCreator - loadRepository( - metaResponse.result.id, - metaResponse.result.dataTxId, - metaResponse.result.uploadStrategy, - metaResponse.result.privateStateTxId - ) + loadRepository(metaResponse.result.id) ) if (repoFetchError) { diff --git a/src/stores/repository-core/types.ts b/src/stores/repository-core/types.ts index 04ae5f4b..7a160cc6 100644 --- a/src/stores/repository-core/types.ts +++ b/src/stores/repository-core/types.ts @@ -1,6 +1,8 @@ +import { Edge, Node } from '@xyflow/react' + import { UserCommit, UserContributionData, UserPROrIssue } from '@/lib/user' import { CommitResult } from '@/types/commit' -import { Deployment, Domain, GithubSync, Repo } from '@/types/repository' +import { BondingCurve, Deployment, Domain, GithubSync, Repo, RepoToken } from '@/types/repository' export interface RepoCoreSlice { repoCoreState: RepoCoreState @@ -12,6 +14,7 @@ export type RepoCoreState = { status: ApiStatus error: unknown | null repo: Repo | null + repoHierarchy: RepoHierarchy statistics: { commits: UserCommit[] pullRequests: UserPROrIssue[] @@ -43,6 +46,11 @@ export type RepoCoreState = { } } +export type RepoHierarchy = { + nodes: Node[] + edges: Edge[] +} + export type RepoCoreActions = { updateRepoName: (name: string) => Promise updateRepoDescription: (description: string) => Promise @@ -65,9 +73,16 @@ export type RepoCoreActions = { fetchAndLoadRepository: (id: string, branchName?: string) => Promise fetchAndLoadParentRepository: (repo: Repo) => Promise fetchAndLoadForkRepository: (id: string) => Promise + fetchRepoHierarchy: () => Promise loadFilesFromRepo: () => Promise reloadFilesOnCurrentFolder: () => Promise setRepoContributionStats: (data: UserContributionData) => void + setRepoDecentralized: () => void + setRepoTokenProcessId: (processId: string) => void + saveRepoTokenDetails: (token: Partial) => Promise + saveRepoBondingCurveDetails: (bondingCurve: BondingCurve) => Promise + saveLiquidityPoolId: (liquidityPoolId: string) => Promise + saveBondingCurveId: (bondingCurveId: string) => Promise isRepoOwner: () => boolean isContributor: () => boolean reset: () => void @@ -95,8 +110,24 @@ export type FileObject = { } export type ForkRepositoryOptions = { + id: string name: string description: string parent: string dataTxId: string } + +export type CurveState = { + reserveBalance: string + initialized: boolean + repoToken: RepoToken + reserveToken: RepoToken + fundingGoal: string + allocationForLP: string + allocationForCreator: string + maxSupply: string + supplyToSell: string + reachedFundingGoal: boolean + liquidityPool?: string + creator: string +} diff --git a/src/types/repository.ts b/src/types/repository.ts index 7b148b3c..4db0412b 100644 --- a/src/types/repository.ts +++ b/src/types/repository.ts @@ -13,6 +13,9 @@ export type Repo = { description: string defaultBranch: string dataTxId: string + token: RepoToken | null | undefined + bondingCurve: BondingCurve | null | undefined + liquidityPoolId: string | null uploadStrategy: 'DEFAULT' | 'ARSEEDING' owner: string pullRequests: PullRequest[] @@ -30,6 +33,50 @@ export type Repo = { privateStateTxId?: string contributorInvites: ContributorInvite[] githubSync: GithubSync | null + decentralized?: boolean + primary?: boolean +} + +export type RepoToken = { + tokenName: string + tokenTicker: string + denomination: string + totalSupply: string + tokenImage: string + allocations: Allocation[] + processId?: string +} + +export type BondingCurve = { + fundingGoal: string + processId?: string + reserveToken: RepoLiquidityPoolToken +} + +export type RepoLiquidityPool = { + quoteToken: RepoLiquidityPoolToken + baseToken: RepoLiquidityPoolToken +} + +export type RepoLiquidityPoolToken = { + tokenName: string + tokenTicker: string + denomination: string + tokenImage: string + processId: string +} + +export type Token = { + name?: string + ticker?: string + processId: string + denomination: number + logo?: string +} + +export type Allocation = { + address: string + percentage: string } export interface GithubSync { diff --git a/yarn.lock b/yarn.lock index 14570981..0aeef68c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2133,7 +2133,7 @@ base64-js "^1.5.1" bignumber.js "^9.1.1" -"@isomorphic-git/idb-keyval@3.3.2": +"@isomorphic-git/idb-keyval@3.3.2", "@isomorphic-git/idb-keyval@^3.3.2": version "3.3.2" resolved "https://registry.yarnpkg.com/@isomorphic-git/idb-keyval/-/idb-keyval-3.3.2.tgz#c0509a6c5987d8a62efb3e47f2815bcc5eda2489" integrity sha512-r8/AdpiS0/WJCNR/t/gsgL+M8NMVj/ek7s60uz3LmpCaTF2mEVlZJlB01ZzalgYzRLXwSPC92o+pdzjM7PN/pA== @@ -2526,6 +2526,28 @@ warp-arbundles "^1.0.4" zod "^3.22.4" +"@protocol.land/isomorphic-git@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protocol.land/isomorphic-git/-/isomorphic-git-1.1.0.tgz#f6cfa1f3790ee611cfc2a2dd72e4b400a8615665" + integrity sha512-RjbjUgeJSyUqXCr8eHPCF5kxQqysxsavzhjoa4yFobUDiLu8/GmWROj3fik506nHa0Ui3MFZ1pVa0DWQESK5Lg== + dependencies: + async-lock "^1.1.0" + clean-git-ref "^2.0.1" + crc-32 "^1.2.0" + diff3 "0.0.3" + ignore "^5.1.4" + minimisted "^2.0.0" + pako "^1.0.10" + pify "^4.0.1" + readable-stream "^3.4.0" + sha.js "^2.4.9" + simple-get "^4.0.1" + +"@ramonak/react-progress-bar@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@ramonak/react-progress-bar/-/react-progress-bar-5.3.0.tgz#b066faf1b332b6a5f51d83b5935da5f8cf1deecf" + integrity sha512-PjpOcSBAVSQNyx2cvYyBCI14Tg2eFM0psC9m2ic33PYBIdOzO9/DieWndq9BUQTSjIIarhSpa/lqJ33W/mFJMw== + "@randlabs/communication-bridge@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@randlabs/communication-bridge/-/communication-bridge-1.0.1.tgz#d1ecfc29157afcbb0ca2d73122d67905eecb5bf3" @@ -2716,6 +2738,11 @@ resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.8.3.tgz#9db61ab2a96e43d9e035b1cfd82eeede6d52f171" integrity sha512-vd2A2TnM5lbnWZnHi9B+L2gPtkSeOtJOAw358JqokIH1+v2J7vUAzFVPwB/wrye12RFOurffXu33plm4uQ+JBQ== +"@types/canvas-confetti@^1.6.4": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@types/canvas-confetti/-/canvas-confetti-1.6.4.tgz#620fd8d78b335d6a4046c0f73236d6988b37552a" + integrity sha512-fNyZ/Fdw/Y92X0vv7B+BD6ysHL4xVU5dJcgzgxLdGbn8O3PezZNIJpml44lKM0nsGur+o/6+NZbZeNTt00U1uA== + "@types/clean-git-ref@^2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@types/clean-git-ref/-/clean-git-ref-2.0.2.tgz#cf81d3ec8228a46d1a7e3a7aa744b3a616419e2f" @@ -2731,11 +2758,25 @@ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.2.tgz#7939eed011a908287cd1bcfd11580c17b2ac7f8a" integrity sha512-At+Ski7dL8Bs58E8g8vPcFJc8tGcaC12Z4m07+p41+DRqnZQcAlp3NfYjLrhNYv+zEyQitU1CUxXNjqUyf+c0g== +"@types/d3-drag@^3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" + integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== + dependencies: + "@types/d3-selection" "*" + "@types/d3-ease@^3.0.0": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.1.tgz#ef386d2f28602dba82206888047f97f7f7f7558a" integrity sha512-VZofjpEt8HWv3nxUAosj5o/+4JflnJ7Bbv07k17VO3T2WRuzGdZeookfaF60iVh5RdhVG49LE5w6LIshVUC6rg== +"@types/d3-interpolate@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + "@types/d3-interpolate@^3.0.1": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.3.tgz#e10c06c4bf11bd770ed56184a0d76cd516ff4ded" @@ -2755,6 +2796,11 @@ dependencies: "@types/d3-time" "*" +"@types/d3-selection@*", "@types/d3-selection@^3.0.10": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe" + integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg== + "@types/d3-shape@^3.1.0": version "3.1.4" resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.4.tgz#748a256d5e499cdfb3e48beca9c557f3ea0ff15c" @@ -2772,6 +2818,21 @@ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.1.tgz#8dac23292df0e559a3aa459d8efca78a734c3fbe" integrity sha512-GGTvzKccVEhxmRfJEB6zhY9ieT4UhGVUIQaBzFpUO9OXy2ycAlnPCSJLzmGGgqt3KVjqN3QCQB4g1rsZnHsWhg== +"@types/d3-transition@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.8.tgz#677707f5eed5b24c66a1918cde05963021351a8f" + integrity sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@^3.0.8": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" + integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + "@types/debug@^4.0.0": version "4.1.8" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317" @@ -2856,6 +2917,11 @@ resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== +"@types/numeral@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@types/numeral/-/numeral-2.0.5.tgz#388e5c4ff4b0e1787f130753cbbe83d3ba770858" + integrity sha512-kH8I7OSSwQu9DS9JYdFWbuvhVzvFRoCPCkGxNwoGgaPeDfEPJlcxNvEOypZhQ3XXHsGbfIuYcxcJxKUfJHnRfw== + "@types/parse5@^6.0.0": version "6.0.3" resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" @@ -3165,6 +3231,28 @@ resolved "https://registry.yarnpkg.com/@weavery/clarity/-/clarity-0.1.5.tgz#f06bbb0dac7c63c6e2ccd76cda3e8b32b57f82c2" integrity sha512-0ms2/sBx+uyW3EmXte5otIzNVAXpfJ3lBl6FS8JuLdWmPU6SxiAoGTMUT0N0SL3Ogiz2PZt6NV+mfApbSvYBaQ== +"@xyflow/react@^12.0.4": + version "12.0.4" + resolved "https://registry.yarnpkg.com/@xyflow/react/-/react-12.0.4.tgz#e3502ed7430e58f5f0c51437dad98e4d33684897" + integrity sha512-eeQzw1gIbLKOB55rp2+20uB1PASDUf1q6zy2VsgugnuPEcL/olVMX3WT42XxyG8m3rcbUiHlq2NSmPTFWEjiUQ== + dependencies: + "@xyflow/system" "0.0.37" + classcat "^5.0.3" + zustand "^4.4.0" + +"@xyflow/system@0.0.37": + version "0.0.37" + resolved "https://registry.yarnpkg.com/@xyflow/system/-/system-0.0.37.tgz#eb656be8a5b6aa29dfb456ce4859454437b47a34" + integrity sha512-hSIhezhxgftPUpC+xiQVIorcRILZUOWlLjpYPTyGWRu8s4RJvM4GqvrsFmD5OnMKXLgpU7/PqqUibDVO67oWQQ== + dependencies: + "@types/d3-drag" "^3.0.7" + "@types/d3-selection" "^3.0.10" + "@types/d3-transition" "^3.0.8" + "@types/d3-zoom" "^3.0.8" + d3-drag "^3.0.0" + d3-selection "^3.0.0" + d3-zoom "^3.0.0" + abbrev@1: version "1.1.1" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" @@ -3284,6 +3372,16 @@ aproba@^1.0.3: resolved "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +ar-gql@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/ar-gql/-/ar-gql-1.2.9.tgz#423583dbebef7e8c838b7634d9d3ffb6d972aa3c" + integrity sha512-LZu4Mt92oFTA+JJ0PdiowJEFS2t6FhKWeYRBo9LTqx9shrIMGRb3iP5SvPbqLREXSSBriJOyyM3tgQ4wDYKr/w== + +ar-gql@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ar-gql/-/ar-gql-2.0.2.tgz#b2c81330628bd9e81396f350c08b749a350e2cab" + integrity sha512-x803rnC20iN9gj1WlnwO+Cqvahu1QmnuwM529OdVzwl2krj/tdNoG4OZ1c1n6ougPVDWv+PNbkBaXggeiI6wDw== + arbundles@^0.10.0: version "0.10.1" resolved "https://registry.npmjs.org/arbundles/-/arbundles-0.10.1.tgz#1f542d9edf185a8a272994aef501a8ee12aaaa46" @@ -3391,6 +3489,22 @@ are-we-there-yet@~1.1.2: delegates "^1.0.0" readable-stream "^2.0.6" +arfs-js@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/arfs-js/-/arfs-js-1.4.2.tgz#2f8d1302dbbd56228ab4dc7fe0e0283dbcdcf5fd" + integrity sha512-fKCYZgmAVFv87/3kZJyNlUFi5op/I+qeejqXmkhIB00npQZqk0w5sq5N518RwlZN/ji+z+VhFUtWFuZNlg+Ozg== + dependencies: + "@isomorphic-git/idb-keyval" "^3.3.2" + "@isomorphic-git/lightning-fs" "^4.6.0" + ar-gql "^1.2.9" + arweave "^1.14.4" + gql-query-builder "^3.8.0" + isomorphic-textencoder "^1.0.1" + just-debounce-it "^3.2.0" + uuid "^9.0.0" + warp-arbundles "^1.0.4" + warp-contracts-plugin-signature "^1.0.20" + arg@^5.0.2: version "5.0.2" resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" @@ -3707,7 +3821,7 @@ bignumber.js@9.1.1, bignumber.js@^9.0.0, bignumber.js@^9.0.2: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.1.tgz#c4df7dc496bd849d4c9464344c1aa74228b4dac6" integrity sha512-pHm4LsMJ6lzgNGVfZHjMoO8sdoRhOzOH4MLmY65Jg70bpxCKu5iOHNJyfF6OyvYw7t8Fpf35RuzUyqnQsj8Vig== -bignumber.js@^9.0.1, bignumber.js@^9.1.1: +bignumber.js@^9.0.1, bignumber.js@^9.1.1, bignumber.js@^9.1.2: version "9.1.2" resolved "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== @@ -3981,6 +4095,11 @@ caniuse-lite@^1.0.30001464, caniuse-lite@^1.0.30001517: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001519.tgz#3e7b8b8a7077e78b0eb054d69e6edf5c7df35601" integrity sha512-0QHgqR+Jv4bxHMp8kZ1Kn8CH55OikjKJ6JmKkZYP1F3D7w+lnFXF70nG5eNfsZS89jadi5Ywy5UCSKLAglIRkg== +canvas-confetti@^1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/canvas-confetti/-/canvas-confetti-1.9.3.tgz#ef4c857420ad8045ab4abe8547261c8cdf229845" + integrity sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g== + catering@^2.1.0, catering@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/catering/-/catering-2.1.1.tgz#66acba06ed5ee28d5286133982a927de9a04b510" @@ -4071,6 +4190,11 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" +classcat@^5.0.3: + version "5.0.5" + resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77" + integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w== + classic-level@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/classic-level/-/classic-level-1.3.0.tgz#5e36680e01dc6b271775c093f2150844c5edd5c8" @@ -4369,7 +4493,20 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== -d3-ease@^3.0.1: +"d3-dispatch@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + +"d3-drag@2 - 3", d3-drag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + +"d3-ease@1 - 3", d3-ease@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== @@ -4379,7 +4516,7 @@ d3-ease@^3.0.1: resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== -"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: +"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== @@ -4402,6 +4539,11 @@ d3-scale@^4.0.2: d3-time "2.1.1 - 3" d3-time-format "2 - 4" +"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + d3-shape@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" @@ -4423,11 +4565,33 @@ d3-shape@^3.1.0: dependencies: d3-array "2 - 3" -d3-timer@^3.0.1: +"d3-timer@1 - 3", d3-timer@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== +"d3-transition@2 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + date-fns@^2.30.0: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" @@ -5060,6 +5224,11 @@ extend@^3.0.0: resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== +fancy-canvas@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fancy-canvas/-/fancy-canvas-2.1.0.tgz#44b40e40419ad8ef8304df365e4276767e918552" + integrity sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ== + fast-copy@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/fast-copy/-/fast-copy-3.0.1.tgz#9e89ef498b8c04c1cd76b33b8e14271658a732aa" @@ -5423,6 +5592,11 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +gql-query-builder@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/gql-query-builder/-/gql-query-builder-3.8.0.tgz#d182d127f88abb7d39f7bec2c64f8b4570812e2f" + integrity sha512-q0PncZTrLDeyiH4R7YH1ISM+XGB4NvQ8eTm/Wr/sHSuquFZvqvDpGyMhbgoCZDc8kNAK8GOdfh3nI2GCLREFvw== + graceful-fs@^4.2.0: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" @@ -6095,24 +6269,7 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== -isomorphic-git@^1.24.5: - version "1.24.5" - resolved "https://registry.yarnpkg.com/isomorphic-git/-/isomorphic-git-1.24.5.tgz#f4816494eae81d94f9fd3a6d657b02cd0a08c456" - integrity sha512-07M4YscftHZJIuw7xZhgWkdFvVjHSBJBsIwWXkxgFCivhb0l8mGNchM7nO2hU27EKSIf0sT4gJivEgLGohWbzA== - dependencies: - async-lock "^1.1.0" - clean-git-ref "^2.0.1" - crc-32 "^1.2.0" - diff3 "0.0.3" - ignore "^5.1.4" - minimisted "^2.0.0" - pako "^1.0.10" - pify "^4.0.1" - readable-stream "^3.4.0" - sha.js "^2.4.9" - simple-get "^4.0.1" - -isomorphic-textencoder@1.0.1: +isomorphic-textencoder@1.0.1, isomorphic-textencoder@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/isomorphic-textencoder/-/isomorphic-textencoder-1.0.1.tgz#38dcd3b4416d29cd33e274f64b99ae567cd15e83" integrity sha512-676hESgHullDdHDsj469hr+7t3i/neBKU9J7q1T4RHaWwLAsaQnywC0D1dIUId0YZ+JtVrShzuBk1soo0+GVcQ== @@ -6220,6 +6377,11 @@ just-debounce-it@1.1.0: resolved "https://registry.yarnpkg.com/just-debounce-it/-/just-debounce-it-1.1.0.tgz#8e92578effc155358a44f458c52ffbee66983bef" integrity sha512-87Nnc0qZKgBZuhFZjYVjSraic0x7zwjhaTMrCKlj0QYKH6lh0KbFzVnfu6LHan03NO7J8ygjeBeD0epejn5Zcg== +just-debounce-it@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/just-debounce-it/-/just-debounce-it-3.2.0.tgz#4352265f4af44188624ce9fdbc6bff4d49c63a80" + integrity sha512-WXzwLL0745uNuedrCsCs3rpmfD6DBaf7uuVwaq98/8dafURfgQaBsSpjiPp5+CW6Vjltwy9cOGI6qE71b3T8iQ== + just-once@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/just-once/-/just-once-1.1.0.tgz#fe81a185ebaeeb0947a7e705bf01cb6808db0ad8" @@ -6300,6 +6462,13 @@ lie@~3.3.0: dependencies: immediate "~3.0.5" +lightweight-charts@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/lightweight-charts/-/lightweight-charts-4.2.1.tgz#a2d97048c026644507faae679e00bf3088c9c247" + integrity sha512-nE2zCZ5Gp7KZbVHUJi6QhQLkYRvYyxsQTnSLEXIFmc8iHOFBT4rk/Dbyecq+CLW59FNuoCPNOYjZnS63/uHDrA== + dependencies: + fancy-canvas "2.1.0" + lilconfig@^2.0.5, lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" @@ -7235,6 +7404,11 @@ number-is-nan@^1.0.0: resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" integrity sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ== +numeral@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/numeral/-/numeral-2.0.6.tgz#4ad080936d443c2561aed9f2197efffe25f4e506" + integrity sha512-qaKRmtYPZ5qdw4jWJD6bxEf1FJEqllJrwxCLIm0sQU/A7v2/czigzOb+C2uSiFsa9lBUzeH7M1oK+Q+OLxL3kA== + object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"