diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..746b184 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,40 @@ +# Client-side environment variables (must be prefixed with NEXT_PUBLIC_) + +# ATOM Payment Configuration +NEXT_PUBLIC_RECIPIENT_ADDRESS=cosmos1yourrealaddress +NEXT_PUBLIC_COSMOS_RPC_URL=https://rpc.cosmos.network +NEXT_PUBLIC_COSMOS_API_URL=https://api.cosmos.network +NEXT_PUBLIC_COSMOS_CHAIN_ID=cosmoshub-4 + +# Solana/GOR Payment Configuration +NEXT_PUBLIC_SOLANA_RPC_URL=https://skilled-prettiest-seed.solana-mainnet.quiknode.pro/eeecfebd04e345f69f1900cc3483cbbfea02a158 +NEXT_PUBLIC_SOLANA_WEBSOCKET_URL=wss://skilled-prettiest-seed.solana-mainnet.quiknode.pro/ +NEXT_PUBLIC_GOR_MINT_ADDRESS=71Jvq4Epe2FCJ7JFSF7jLXdNk1Wy4Bhqd9iL6bEFELvg +NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS= + +# UI Configuration (optional) +NEXT_PUBLIC_DOMAIN_SUFFIX= +NEXT_PUBLIC_EXAMPLE_URL=https://github.com/cerc-io/laconic-registry-cli + +# Server-side environment variables + +# Laconic Registry Configuration +REGISTRY_CHAIN_ID= +REGISTRY_GQL_ENDPOINT= +REGISTRY_RPC_ENDPOINT= +REGISTRY_BOND_ID= +REGISTRY_AUTHORITY= +REGISTRY_USER_KEY= +REGISTRY_GAS=900000 +REGISTRY_FEES=900000alnt +REGISTRY_GAS_PRICE=0.001 + +# Application Configuration +APP_NAME=atom-deploy +DEPLOYER_LRN= + +# LNT Transfer Configuration (required for both ATOM and GOR flows) +# Note: REGISTRY_USER_KEY is used as the prefilled account for LNT transfers +# TODO: Use deployer lrn to determine the address +LACONIC_SERVICE_PROVIDER_ADDRESS= +LACONIC_TRANSFER_AMOUNT=1000000alnt diff --git a/.gitignore b/.gitignore index 5ef6a52..24c06b8 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,7 @@ yarn-error.log* .pnpm-debug.log* # env files (can opt-in for committing if needed) -.env* +.env.local # vercel .vercel diff --git a/README.md b/README.md index 5d5ebc3..0fd2826 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ The DNS names are generated with the following format: {sanitized-url-name}-{short-commit-hash}-{random-salt}{domain-suffix} ``` -For example: +For example: - Basic DNS: `github-abc123-xyz789` - With domain suffix: `github-abc123-xyz789.example.com` diff --git a/package-lock.json b/package-lock.json index 82b51f7..49103be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,10 @@ "@cerc-io/registry-sdk": "^0.2.11", "@cosmjs/stargate": "^0.32.3", "@keplr-wallet/types": "^0.12.71", + "@solana/spl-token": "^0.4.13", + "@solana/web3.js": "^1.98.2", "axios": "^1.6.8", + "bn.js": "^5.2.2", "next": "15.3.1", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -40,6 +43,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cerc-io/registry-sdk": { "version": "0.2.11", "resolved": "https://git.vdb.to/api/packages/cerc-io/npm/%40cerc-io%2Fregistry-sdk/-/0.2.11/registry-sdk-0.2.11.tgz", @@ -1918,7 +1930,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.7.0.tgz", "integrity": "sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==", - "peer": true, "dependencies": { "@noble/hashes": "1.6.0" }, @@ -1933,7 +1944,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.6.0.tgz", "integrity": "sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -2104,6 +2114,290 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@solana/buffer-layout": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz", + "integrity": "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==", + "license": "MIT", + "dependencies": { + "buffer": "~6.0.3" + }, + "engines": { + "node": ">=5.10" + } + }, + "node_modules/@solana/buffer-layout-utils": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz", + "integrity": "sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==", + "license": "Apache-2.0", + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/web3.js": "^1.32.0", + "bigint-buffer": "^1.1.5", + "bignumber.js": "^9.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@solana/codecs": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-rc.1.tgz", + "integrity": "sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-data-structures": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/codecs-strings": "2.0.0-rc.1", + "@solana/options": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz", + "integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-data-structures": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz", + "integrity": "sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz", + "integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-strings": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz", + "integrity": "sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22", + "typescript": ">=5" + } + }, + "node_modules/@solana/errors": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz", + "integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.1.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/errors/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@solana/options": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-rc.1.tgz", + "integrity": "sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-data-structures": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/codecs-strings": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/spl-token": { + "version": "0.4.13", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.4.13.tgz", + "integrity": "sha512-cite/pYWQZZVvLbg5lsodSovbetK/eA24gaR0eeUeMuBAMNrT8XFCwaygKy0N2WSg3gSyjjNpIeAGBAKZaY/1w==", + "license": "Apache-2.0", + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-group": "^0.0.7", + "@solana/spl-token-metadata": "^0.1.6", + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.5" + } + }, + "node_modules/@solana/spl-token-group": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz", + "integrity": "sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==", + "license": "Apache-2.0", + "dependencies": { + "@solana/codecs": "2.0.0-rc.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.3" + } + }, + "node_modules/@solana/spl-token-metadata": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz", + "integrity": "sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==", + "license": "Apache-2.0", + "dependencies": { + "@solana/codecs": "2.0.0-rc.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.3" + } + }, + "node_modules/@solana/web3.js": { + "version": "1.98.2", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.98.2.tgz", + "integrity": "sha512-BqVwEG+TaG2yCkBMbD3C4hdpustR4FpuUFRPUmqRZYYlPI9Hg4XMWxHWOWRzHE9Lkc9NDjzXFX7lDXSgzC7R1A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "@noble/curves": "^1.4.2", + "@noble/hashes": "^1.4.0", + "@solana/buffer-layout": "^4.0.1", + "@solana/codecs-numbers": "^2.1.0", + "agentkeepalive": "^4.5.0", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.1", + "node-fetch": "^2.7.0", + "rpc-websockets": "^9.0.2", + "superstruct": "^2.0.2" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-core": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.3.0.tgz", + "integrity": "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/codecs-numbers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.3.0.tgz", + "integrity": "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.3.0", + "@solana/errors": "2.3.0" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/@solana/errors": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.3.0.tgz", + "integrity": "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^14.0.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.18.0" + }, + "peerDependencies": { + "typescript": ">=5.3.3" + } + }, + "node_modules/@solana/web3.js/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@solana/web3.js/node_modules/commander": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -2468,6 +2762,15 @@ "@types/node": "*" } }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -2538,6 +2841,21 @@ "@types/node": "*" } }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.31.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", @@ -3027,6 +3345,18 @@ "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==" }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3345,6 +3675,28 @@ "node": ">=0.6" } }, + "node_modules/bigint-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/bigint-buffer/-/bigint-buffer-1.1.5.tgz", + "integrity": "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "bindings": "^1.3.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bindings": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", @@ -3390,7 +3742,19 @@ "node_modules/bn.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==" + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "license": "MIT" + }, + "node_modules/borsh": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/borsh/-/borsh-0.7.0.tgz", + "integrity": "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==", + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0", + "bs58": "^4.0.0", + "text-encoding-utf-8": "^1.0.2" + } }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -3477,6 +3841,20 @@ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" }, + "node_modules/bufferutil": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.9.tgz", + "integrity": "sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -3694,6 +4072,15 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -3875,6 +4262,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4117,6 +4516,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4670,6 +5084,12 @@ "npm": ">=3" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -4679,6 +5099,14 @@ "safe-buffer": "^5.1.1" } }, + "node_modules/eyes": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "engines": { + "node": "> 0.1.90" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4724,6 +5152,19 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-stable-stringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", + "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==", + "license": "MIT" + }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "license": "CC0-1.0", + "peer": true + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -5190,6 +5631,15 @@ "minimalistic-crypto-utils": "^1.0.1" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -5713,6 +6163,44 @@ "node": ">= 0.4" } }, + "node_modules/jayson": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/jayson/-/jayson-4.2.0.tgz", + "integrity": "sha512-VfJ9t1YLwacIubLhONk0KFeosUBwstRWQ0IRT1KDjEjnVnSOVHC3uwugyV7L0c7R9lpVyrUGT2XWiBA1UTtpyg==", + "license": "MIT", + "dependencies": { + "@types/connect": "^3.4.33", + "@types/node": "^12.12.54", + "@types/ws": "^7.4.4", + "commander": "^2.20.3", + "delay": "^5.0.0", + "es6-promisify": "^5.0.0", + "eyes": "^0.1.8", + "isomorphic-ws": "^4.0.1", + "json-stringify-safe": "^5.0.1", + "stream-json": "^1.9.1", + "uuid": "^8.3.2", + "ws": "^7.5.10" + }, + "bin": { + "jayson": "bin/jayson.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jayson/node_modules/@types/node": { + "version": "12.20.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz", + "integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==", + "license": "MIT" + }, + "node_modules/jayson/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -5768,6 +6256,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, "node_modules/json5": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", @@ -6341,8 +6835,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/multiformats": { "version": "9.9.0", @@ -6481,7 +6974,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -7121,6 +7613,59 @@ "rlp": "bin/rlp" } }, + "node_modules/rpc-websockets": { + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-9.1.1.tgz", + "integrity": "sha512-1IXGM/TfPT6nfYMIXkJdzn+L4JEsmb0FL1O2OBjaH03V3yuUDdKFulGLMFG6ErV+8pZ5HVC0limve01RyO+saA==", + "license": "LGPL-3.0-only", + "dependencies": { + "@swc/helpers": "^0.5.11", + "@types/uuid": "^8.3.4", + "@types/ws": "^8.2.2", + "buffer": "^6.0.3", + "eventemitter3": "^5.0.1", + "uuid": "^8.3.2", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + } + }, + "node_modules/rpc-websockets/node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/rpc-websockets/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -7562,6 +8107,21 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/stream-chain": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", + "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", + "license": "BSD-3-Clause" + }, + "node_modules/stream-json": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", + "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", + "license": "BSD-3-Clause", + "dependencies": { + "stream-chain": "^2.2.5" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -7772,6 +8332,15 @@ } } }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7818,6 +8387,11 @@ "node": ">=6" } }, + "node_modules/text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, "node_modules/tiny-secp256k1": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-1.1.7.tgz", @@ -7920,8 +8494,7 @@ "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "peer": true + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/ts-api-utils": { "version": "2.1.0", @@ -8063,7 +8636,6 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8155,16 +8727,38 @@ "requires-port": "^1.0.0" } }, + "node_modules/utf-8-validate": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", + "integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "peer": true + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/whatwg-fetch": { "version": "3.6.20", @@ -8176,7 +8770,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/package.json b/package.json index 74fec5c..5631c72 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,10 @@ "@cerc-io/registry-sdk": "^0.2.11", "@cosmjs/stargate": "^0.32.3", "@keplr-wallet/types": "^0.12.71", + "@solana/spl-token": "^0.4.13", + "@solana/web3.js": "^1.98.2", "axios": "^1.6.8", + "bn.js": "^5.2.2", "next": "15.3.1", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/src/app/api/registry/route.ts b/src/app/api/registry/route.ts index fb3c346..8effcf0 100644 --- a/src/app/api/registry/route.ts +++ b/src/app/api/registry/route.ts @@ -2,13 +2,15 @@ import { NextRequest, NextResponse } from 'next/server'; import { Account, Registry, parseGasAndFees } from '@cerc-io/registry-sdk'; import { GasPrice } from '@cosmjs/stargate'; import axios from 'axios'; +import { verifyUnusedSolanaPayment } from '@/utils/solanaVerify'; +import { transferLNTTokens } from '@/services/laconicTransfer'; // Sleep helper function const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); // ATOM payment verification function -const verifyAtomPayment = async (txHash: string): Promise<{ - valid: boolean, +const verifyAtomPayment = async (txHash: string): Promise<{ + valid: boolean, reason?: string, amount?: string, sender?: string @@ -16,18 +18,18 @@ const verifyAtomPayment = async (txHash: string): Promise<{ try { const apiEndpoint = process.env.NEXT_PUBLIC_COSMOS_API_URL; const recipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS; - const minPaymentUAtom = '100000'; // 0.1 ATOM in uatom - + const minPaymentUAtom = '100000'; // 0.1 ATOM in uatom + if (!apiEndpoint) { - return { - valid: false, + return { + valid: false, reason: 'ATOM API endpoint not configured' }; } - + if (!recipientAddress) { - return { - valid: false, + return { + valid: false, reason: 'ATOM recipient address not configured' }; } @@ -36,8 +38,8 @@ const verifyAtomPayment = async (txHash: string): Promise<{ const response = await axios.get(`${apiEndpoint}/cosmos/tx/v1beta1/txs/${txHash}`); if (!response.data || !response.data.tx || !response.data.tx_response) { - return { - valid: false, + return { + valid: false, reason: 'Invalid transaction data from API endpoint' }; } @@ -45,8 +47,8 @@ const verifyAtomPayment = async (txHash: string): Promise<{ // Check if transaction was successful const txResponse = response.data.tx_response; if (txResponse.code !== 0) { - return { - valid: false, + return { + valid: false, reason: `Transaction failed with code ${txResponse.code}: ${txResponse.raw_log}` }; } @@ -56,10 +58,10 @@ const verifyAtomPayment = async (txHash: string): Promise<{ const now = new Date(); const timeDiffMs = now.getTime() - txTimestamp.getTime(); const timeWindowMs = 5 * 60 * 1000; // 5 minutes - + if (timeDiffMs > timeWindowMs) { - return { - valid: false, + return { + valid: false, reason: `Transaction is older than 5 minutes (${Math.round(timeDiffMs / 60000)} minutes old)` }; } @@ -69,7 +71,7 @@ const verifyAtomPayment = async (txHash: string): Promise<{ let foundValidPayment = false; let paymentAmountUAtom = ''; let sender = ''; - + // Get the sender address from the first signer if (tx.auth_info && tx.auth_info.signer_infos && tx.auth_info.signer_infos.length > 0) { sender = tx.auth_info.signer_infos[0].public_key.address || ''; @@ -80,10 +82,10 @@ const verifyAtomPayment = async (txHash: string): Promise<{ if (msg['@type'] === '/cosmos.bank.v1beta1.MsgSend') { if (msg.to_address === recipientAddress) { for (const coin of msg.amount) { - if (coin.denom === 'uatom') { + if (coin.denom === 'uatom') { // Get the amount in uatom paymentAmountUAtom = coin.amount; - + if (parseInt(paymentAmountUAtom) >= parseInt(minPaymentUAtom)) { foundValidPayment = true; } @@ -95,8 +97,8 @@ const verifyAtomPayment = async (txHash: string): Promise<{ } if (!foundValidPayment) { - return { - valid: false, + return { + valid: false, reason: `Payment amount (${paymentAmountUAtom || '0'}uatom) is less than required (${minPaymentUAtom}uatom) or not sent to the correct address (${recipientAddress})` }; } @@ -108,9 +110,9 @@ const verifyAtomPayment = async (txHash: string): Promise<{ }; } catch (error) { console.error('Error verifying ATOM payment:', error); - return { - valid: false, - reason: `Failed to verify transaction: ${error.message || 'Unknown error'}` + return { + valid: false, + reason: `Failed to verify transaction: ${error instanceof Error ? error.message : 'Unknown error'}` }; } }; @@ -120,16 +122,16 @@ const extractRepoInfo = (url: string): { repoName: string, repoUrl: string, prov try { const parsedUrl = new URL(url); const pathParts = parsedUrl.pathname.split('/').filter(part => part); - + // GitHub repository URL pattern if (parsedUrl.hostname === 'github.com' && pathParts.length >= 2) { return { - repoName: pathParts[1], + repoName: pathParts[1], repoUrl: `https://github.com/${pathParts[0]}/${pathParts[1]}`, provider: 'github' }; } - + // GitLab repository URL pattern if ((parsedUrl.hostname === 'gitlab.com' || parsedUrl.hostname.includes('gitlab')) && pathParts.length >= 2) { return { @@ -138,7 +140,7 @@ const extractRepoInfo = (url: string): { repoName: string, repoUrl: string, prov provider: 'gitlab' }; } - + // Bitbucket repository URL pattern if (parsedUrl.hostname === 'bitbucket.org' && pathParts.length >= 2) { return { @@ -147,7 +149,7 @@ const extractRepoInfo = (url: string): { repoName: string, repoUrl: string, prov provider: 'bitbucket' }; } - + // For other URLs, try to extract a meaningful name from the hostname const hostnameWithoutTLD = parsedUrl.hostname.split('.')[0]; return { @@ -175,7 +177,7 @@ const fetchLatestCommitHash = async (repoUrl: string, provider: string): Promise if (match) { const [, owner, repo] = match; const apiUrl = `https://api.github.com/repos/${owner}/${repo}/commits/main`; - + const response = await axios.get(apiUrl); if (response.data && response.data.sha) { // Return both full hash and short hash (7 characters) @@ -186,7 +188,7 @@ const fetchLatestCommitHash = async (repoUrl: string, provider: string): Promise } } } - + // For non-GitHub repositories or if fetching fails, return a default value return { fullHash: 'main', @@ -208,100 +210,160 @@ const registryTransactionWithRetry = async ( delay = 1000 ): Promise => { let lastError; - + for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await txFn(); } catch (error) { console.error(`Transaction attempt ${attempt + 1} failed:`, error); lastError = error; - + if (attempt < maxRetries - 1) { await sleep(delay); } } } - + throw lastError; }; export async function POST(request: NextRequest) { try { // First check if the request body is valid JSON - let url, txHash; + let url, txHash, paymentType; try { const body = await request.json(); url = body.url; txHash = body.txHash; - + paymentType = body.paymentType || 'ATOM'; // Default to ATOM for backward compatibility + if (!url || !txHash) { - return NextResponse.json({ - status: 'error', - message: 'Missing required fields: url and txHash are required' + return NextResponse.json({ + status: 'error', + message: 'Missing required fields: url and txHash are required' + }, { status: 400 }); + } + + if (!['ATOM', 'GOR'].includes(paymentType)) { + return NextResponse.json({ + status: 'error', + message: 'Invalid payment type. Must be ATOM or GOR' }, { status: 400 }); } } catch (error) { - return NextResponse.json({ - status: 'error', - message: 'Invalid JSON in request body' - }, { status: 400 }); - } - - // First, verify the ATOM payment before doing anything else - console.log('Step 0: Verifying ATOM payment...'); - const paymentVerificationResult = await verifyAtomPayment(txHash); - - if (!paymentVerificationResult.valid) { - console.error('ATOM payment verification failed:', paymentVerificationResult.reason); return NextResponse.json({ status: 'error', - message: `Payment verification failed: ${paymentVerificationResult.reason}` + message: 'Invalid JSON in request body' }, { status: 400 }); } - - console.log('ATOM payment verified successfully:', { - amount: paymentVerificationResult.amount, - sender: paymentVerificationResult.sender - }); - // Validate required environment variables - const requiredEnvVars = [ + // Verify payment based on type + if (paymentType === 'ATOM') { + console.log('Step 0: Verifying ATOM payment...'); + const paymentVerificationResult = await verifyAtomPayment(txHash); + + if (!paymentVerificationResult.valid) { + console.error('ATOM payment verification failed:', paymentVerificationResult.reason); + return NextResponse.json({ + status: 'error', + message: `Payment verification failed: ${paymentVerificationResult.reason}` + }, { status: 400 }); + } + + console.log('ATOM payment verified successfully:', { + amount: paymentVerificationResult.amount, + sender: paymentVerificationResult.sender + }); + } else if (paymentType === 'GOR') { + console.log('Step 0: Verifying GOR payment...'); + const gorPaymentResult = await verifyUnusedSolanaPayment(txHash); + + if (!gorPaymentResult.valid) { + console.error('GOR payment verification failed:', gorPaymentResult.reason); + return NextResponse.json({ + status: 'error', + message: `Payment verification failed: ${gorPaymentResult.reason}` + }, { status: 400 }); + } + + console.log('GOR payment verified successfully:', { + amount: gorPaymentResult.amount, + sender: gorPaymentResult.sender + }); + } + + // For both payment types, perform LNT transfer after payment verification + console.log('Step 0.5: Performing LNT transfer from prefilled account to service provider...'); + const lntTransferResult = await transferLNTTokens(); + + if (!lntTransferResult.success) { + console.error('LNT transfer failed:', lntTransferResult.error); + return NextResponse.json({ + status: 'error', + message: `LNT transfer failed: ${lntTransferResult.error}` + }, { status: 500 }); + } + + console.log('LNT transfer completed:', lntTransferResult.transactionHash); + const finalTxHash = lntTransferResult.transactionHash!; // Use LNT transfer hash for registry + + // Validate required environment variables based on payment type + const baseRequiredEnvVars = [ 'REGISTRY_CHAIN_ID', 'REGISTRY_GQL_ENDPOINT', 'REGISTRY_RPC_ENDPOINT', 'REGISTRY_BOND_ID', 'REGISTRY_AUTHORITY', - 'REGISTRY_USER_KEY', + 'REGISTRY_USER_KEY', // This is the same as the prefilled account for LNT transfers 'DEPLOYER_LRN', + // LNT transfer variables now required for both payment types + 'LACONIC_SERVICE_PROVIDER_ADDRESS', + 'LACONIC_TRANSFER_AMOUNT' + ]; + + const atomRequiredEnvVars = [ 'NEXT_PUBLIC_RECIPIENT_ADDRESS', 'NEXT_PUBLIC_COSMOS_API_URL' ]; + const gorRequiredEnvVars = [ + 'NEXT_PUBLIC_SOLANA_RPC_URL', + 'NEXT_PUBLIC_GOR_MINT_ADDRESS', + 'NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS' + ]; + + let requiredEnvVars = [...baseRequiredEnvVars]; + if (paymentType === 'ATOM') { + requiredEnvVars = [...requiredEnvVars, ...atomRequiredEnvVars]; + } else if (paymentType === 'GOR') { + requiredEnvVars = [...requiredEnvVars, ...gorRequiredEnvVars]; + } + for (const envVar of requiredEnvVars) { if (!process.env[envVar]) { console.error(`Missing environment variable: ${envVar}`); - return NextResponse.json({ - status: 'error', - message: `Server configuration error: Missing environment variable: ${envVar}` + return NextResponse.json({ + status: 'error', + message: `Server configuration error: Missing environment variable: ${envVar}` }, { status: 500 }); } } - + // Extract repository information from URL const { repoName, repoUrl, provider } = extractRepoInfo(url); console.log(`Extracted repo info - Name: ${repoName}, URL: ${repoUrl}, Provider: ${provider}`); - + // Fetch latest commit hash (or default to 'main' if unable to fetch) const { fullHash, shortHash } = await fetchLatestCommitHash(repoUrl, provider); console.log(`Using commit hash - Full: ${fullHash}, Short: ${shortHash}`); - + // Use the repository name as the app name const appName = repoName; console.log(`Using app name: ${appName}`); - + // Sanitize the app name to ensure it's DNS-compatible (only alphanumeric and dashes) const sanitizedAppName = appName.replace(/[^a-zA-Z0-9-]/g, '-').toLowerCase(); - + // Generate a random salt (6 alphanumeric characters) to prevent name collisions const generateSalt = (): string => { const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; @@ -309,16 +371,16 @@ export async function POST(request: NextRequest) { }; const salt = generateSalt(); console.log(`Generated salt: ${salt}`); - + // Create DNS name in format: app_name-shortcommithash-salt const dnsName = `${sanitizedAppName}-${shortHash}-${salt}`; console.log(`DNS name with salt: ${dnsName} (sanitized from: ${appName})`); - + // Ensure the DNS name doesn't have consecutive dashes or start/end with a dash let cleanDnsName = dnsName .replace(/--+/g, '-') // Replace consecutive dashes with a single dash .replace(/^-+|-+$/g, ''); // Remove leading and trailing dashes - + // Ensure DNS name is valid (63 chars max per label, all lowercase, starts with a letter) if (cleanDnsName.length > 63) { // If too long, truncate but preserve both the commit hash and salt parts @@ -326,15 +388,15 @@ export async function POST(request: NextRequest) { const maxAppNameLength = 63 - suffixPart.length; cleanDnsName = sanitizedAppName.substring(0, maxAppNameLength) + suffixPart; } - + // If the DNS name ended up empty (unlikely) or doesn't start with a letter (possible), // add a prefix to make it valid if (!cleanDnsName || !/^[a-z]/.test(cleanDnsName)) { cleanDnsName = `app-${cleanDnsName}`; } - + console.log(`Final DNS name with salt: ${cleanDnsName}`); - + // Set up Registry config const config = { chainId: process.env.REGISTRY_CHAIN_ID!, @@ -349,31 +411,31 @@ export async function POST(request: NextRequest) { gasPrice: '0.001alnt', // Hardcoded valid gas price string with denomination }, }; - + console.log('Registry config:', { ...config, privateKey: '[REDACTED]', // Don't log the private key }); - + const deployerLrn = process.env.DEPLOYER_LRN!; - + // Create Registry client instance const gasPrice = GasPrice.fromString('0.001alnt'); console.log('Using manual gas price:', gasPrice); - + const registry = new Registry( config.gqlEndpoint, config.rpcEndpoint, { chainId: config.chainId, gasPrice } ); - + // Create LRN for the application with commit hash and salt // We already have the salt from earlier, so we use it directly const lrn = `lrn://${config.authority}/applications/${appName}-${shortHash}-${salt}`; - + // Get current timestamp for the meta note const timestamp = new Date().toUTCString(); - + // Step 1: Create and publish ApplicationRecord first console.log('Step 1: Publishing ApplicationRecord...'); const applicationRecord = { @@ -385,19 +447,19 @@ export async function POST(request: NextRequest) { repository_ref: fullHash, app_version: '0.0.1' }; - + // Create fee for transaction directly const fee = { amount: [{ denom: 'alnt', amount: process.env.REGISTRY_FEES?.replace('alnt', '') || '900000' }], gas: process.env.REGISTRY_GAS || '900000', }; - + console.log('Application record data:', applicationRecord); - + // Publish the application record let applicationRecordId; try { - const appRecordResult = await registryTransactionWithRetry(() => + const appRecordResult = await registryTransactionWithRetry(() => registry.setRecord( { privateKey: config.privateKey, @@ -408,10 +470,10 @@ export async function POST(request: NextRequest) { fee ) ) as { id?: string }; - + applicationRecordId = appRecordResult.id; console.log('Application record published with ID:', applicationRecordId); - + if (!applicationRecordId) { return NextResponse.json({ status: 'error', @@ -425,7 +487,7 @@ export async function POST(request: NextRequest) { message: err instanceof Error ? err.message : 'Unknown error publishing ApplicationRecord' }, { status: 500 }); } - + // Step 2: Set name mappings console.log('Step 2: Setting name mappings...'); try { @@ -441,7 +503,7 @@ export async function POST(request: NextRequest) { ) ); console.log(`Set name mapping: ${lrn} -> ${applicationRecordId}`); - + // Set the versioned LRN (with repository_ref) await registryTransactionWithRetry(() => registry.setName( @@ -461,7 +523,7 @@ export async function POST(request: NextRequest) { message: err instanceof Error ? err.message : 'Unknown error setting name mappings' }, { status: 500 }); } - + // Step 3: Create ApplicationDeploymentRequest console.log('Step 3: Creating ApplicationDeploymentRequest...'); // Prepare record data for deployment request @@ -482,15 +544,15 @@ export async function POST(request: NextRequest) { repository: repoUrl, repository_ref: fullHash, }, - payment: txHash, + payment: finalTxHash, }; - + console.log('Deployment request data:', deploymentRequestData); - + // Publish the deployment request let deploymentRequestId; try { - const deployRequestResult = await registryTransactionWithRetry(() => + const deployRequestResult = await registryTransactionWithRetry(() => registry.setRecord( { privateKey: config.privateKey, @@ -501,10 +563,10 @@ export async function POST(request: NextRequest) { fee ) ) as { id?: string }; - + deploymentRequestId = deployRequestResult.id; console.log('Deployment request published with ID:', deploymentRequestId); - + if (!deploymentRequestId) { return NextResponse.json({ status: 'error', @@ -518,7 +580,7 @@ export async function POST(request: NextRequest) { message: err instanceof Error ? err.message : 'Unknown error publishing deployment request' }, { status: 500 }); } - + // Return combined results return NextResponse.json({ id: deploymentRequestId, diff --git a/src/app/page.tsx b/src/app/page.tsx index fb4b67d..ff72398 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,17 +1,24 @@ 'use client'; import { useState } from 'react'; -// Dynamically import Keplr component to avoid SSR issues with browser APIs +// Dynamically import components to avoid SSR issues with browser APIs const KeplrConnect = dynamic(() => import('@/components/KeplrConnect'), { ssr: false }); -import URLForm from '@/components/URLForm'; -// Dynamically import PaymentModal component to avoid SSR issues with browser APIs +const SolanaConnect = dynamic(() => import('@/components/SolanaConnect'), { ssr: false }); const PaymentModal = dynamic(() => import('@/components/PaymentModal'), { ssr: false }); +import URLForm from '@/components/URLForm'; import StatusDisplay from '@/components/StatusDisplay'; import { createApplicationDeploymentRequest } from '@/services/registry'; +import { PaymentType, SolanaWalletState } from '@/types'; import dynamic from 'next/dynamic'; export default function Home() { + const [paymentType, setPaymentType] = useState('ATOM'); const [walletAddress, setWalletAddress] = useState(null); + const [solanaWalletState, setSolanaWalletState] = useState({ + connected: false, + publicKey: null, + walletType: null + }); const [url, setUrl] = useState(null); const [showPaymentModal, setShowPaymentModal] = useState(false); const [status, setStatus] = useState<'idle' | 'creating' | 'success' | 'error'>('idle'); @@ -30,6 +37,27 @@ export default function Home() { setWalletAddress(address); }; + const handleSolanaConnect = (walletState: SolanaWalletState) => { + setSolanaWalletState(walletState); + // Store wallet info globally for PaymentModal access (simplified approach) + if (typeof window !== 'undefined') { + (window as any).solanaWalletInfo = { + publicKey: walletState.publicKey, + walletType: walletState.walletType + }; + } + }; + + const handlePaymentTypeChange = (type: PaymentType) => { + setPaymentType(type); + // Reset wallet states when switching payment types + if (type === 'ATOM') { + setSolanaWalletState({ connected: false, publicKey: null, walletType: null }); + } else { + setWalletAddress(null); + } + }; + const handleUrlSubmit = (submittedUrl: string) => { setUrl(submittedUrl); setShowPaymentModal(true); @@ -43,7 +71,7 @@ export default function Home() { try { // Create the Laconic Registry record (payment verification is done in the API) if (url) { - const result = await createApplicationDeploymentRequest(url, hash); + const result = await createApplicationDeploymentRequest(url, hash, paymentType); if (result.status === 'success') { setRecordId(result.id); @@ -89,27 +117,68 @@ export default function Home() {

- Deploy Frontends with ATOM and Laconic + Deploy Frontends with ATOM/GOR + Laconic

+ {/* Payment Type Selection */} +
+

+ 1 + Select Payment Method +

+
+ + +
+
+

1 + style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>2 Connect Your Wallet

- + {paymentType === 'ATOM' ? ( + + ) : ( + + )}
-
+

2 + style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>3 Enter URL to Deploy

@@ -117,7 +186,7 @@ export default function Home() {

3 + style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>4 Deployment Status

- {showPaymentModal && walletAddress && url && ( + {showPaymentModal && url && (paymentType === 'ATOM' ? walletAddress : solanaWalletState.connected) && ( )} diff --git a/src/components/PaymentModal.tsx b/src/components/PaymentModal.tsx index b533c3d..3ab9dfc 100644 --- a/src/components/PaymentModal.tsx +++ b/src/components/PaymentModal.tsx @@ -2,26 +2,27 @@ import { useState } from 'react'; import { sendAtomPayment } from '@/services/keplr'; - -interface PaymentModalProps { - isOpen: boolean; - onClose: () => void; - url: string; - onPaymentComplete: (txHash: string) => void; -} +import { sendGorPayment } from '@/services/solana'; +import { PaymentModalProps } from '@/types'; +import { getSolanaConfig, GOR_PAYMENT_AMOUNT } from '@/config'; export default function PaymentModal({ isOpen, onClose, url, + paymentType, onPaymentComplete, }: PaymentModalProps) { - const [amount, setAmount] = useState('0.01'); + const [amount, setAmount] = useState(paymentType === 'ATOM' ? '0.01' : GOR_PAYMENT_AMOUNT.toString()); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); - // Get recipient address from environment variables - const recipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS || 'cosmos1yourrealaddress'; + // Get recipient addresses from environment variables + const atomRecipientAddress = process.env.NEXT_PUBLIC_RECIPIENT_ADDRESS || 'cosmos1yourrealaddress'; + const gorRecipientAddress = paymentType === 'GOR' ? + (process.env.NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS || 'solana_recipient_address') : ''; + + const recipientAddress = paymentType === 'ATOM' ? atomRecipientAddress : gorRecipientAddress; // Validate amount on change const handleAmountChange = (e: React.ChangeEvent) => { @@ -46,12 +47,30 @@ export default function PaymentModal({ setError(''); try { - const result = await sendAtomPayment(recipientAddress, amount); - - if (result.status === 'success' && result.hash) { - onPaymentComplete(result.hash); - } else { - setError(result.message || 'Payment failed. Please try again.'); + if (paymentType === 'ATOM') { + const result = await sendAtomPayment(recipientAddress, amount); + + if (result.status === 'success' && result.hash) { + onPaymentComplete(result.hash); + } else { + setError(result.message || 'ATOM payment failed. Please try again.'); + } + } else if (paymentType === 'GOR') { + // For GOR payments, we need wallet info from parent component + // This is a simplified approach - in a real implementation, you'd pass wallet state + const walletInfo = (window as any).solanaWalletInfo; + if (!walletInfo || !walletInfo.publicKey || !walletInfo.walletType) { + setError('Solana wallet not connected. Please connect your wallet first.'); + return; + } + + const result = await sendGorPayment(walletInfo.publicKey, walletInfo.walletType); + + if (result.success && result.transactionSignature) { + onPaymentComplete(result.transactionSignature); + } else { + setError(result.error || 'GOR payment failed. Please try again.'); + } } } catch (error) { setError(error instanceof Error ? error.message : 'Payment failed. Please try again.'); @@ -67,7 +86,9 @@ export default function PaymentModal({
-

Complete Payment

+

+ Complete {paymentType} Payment +

@@ -87,27 +108,34 @@ export default function PaymentModal({
- ATOM + {paymentType}
+ {paymentType === 'GOR' && ( +

+ Fixed amount required for deployment +

+ )}
{error && ( @@ -146,7 +174,7 @@ export default function PaymentModal({ )} - {loading ? 'Processing...' : 'Pay with Keplr'} + {loading ? 'Processing...' : `Pay with ${paymentType === 'ATOM' ? 'Keplr' : 'Solana Wallet'}`}
diff --git a/src/components/SolanaConnect.tsx b/src/components/SolanaConnect.tsx new file mode 100644 index 0000000..fbd93f2 --- /dev/null +++ b/src/components/SolanaConnect.tsx @@ -0,0 +1,148 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { connectSolanaWallet, disconnectSolanaWallet, checkSolanaWalletConnection } from '@/services/solana'; +import { SolanaWalletType, SolanaWalletState } from '@/types'; + +interface SolanaConnectProps { + onConnect: (walletState: SolanaWalletState) => void; +} + +export default function SolanaConnect({ onConnect }: SolanaConnectProps) { + const [connecting, setConnecting] = useState(false); + const [walletState, setWalletState] = useState({ + connected: false, + publicKey: null, + walletType: null + }); + + const handleConnect = async (walletType: SolanaWalletType) => { + setConnecting(true); + try { + const newWalletState = await connectSolanaWallet(walletType); + setWalletState(newWalletState); + onConnect(newWalletState); + } catch (error) { + console.error('Failed to connect to Solana wallet:', error); + alert(error instanceof Error ? error.message : 'Failed to connect wallet'); + } finally { + setConnecting(false); + } + }; + + const handleDisconnect = async () => { + if (walletState.walletType) { + try { + await disconnectSolanaWallet(walletState.walletType); + const disconnectedState = { + connected: false, + publicKey: null, + walletType: null + }; + setWalletState(disconnectedState); + onConnect(disconnectedState); + } catch (error) { + console.error('Failed to disconnect wallet:', error); + } + } + }; + + useEffect(() => { + // Check for auto-connection on page load + const checkConnection = () => { + if (typeof window !== 'undefined') { + // Check Phantom + if (window.phantom?.solana && checkSolanaWalletConnection('phantom')) { + handleConnect('phantom'); + return; + } + + // Check Solflare + if (window.solflare && checkSolanaWalletConnection('solflare')) { + handleConnect('solflare'); + return; + } + } + }; + + checkConnection(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( +
+ {walletState.connected ? ( +
+
+ +

+ Connected ({walletState.walletType}) +

+
+
+

{walletState.publicKey}

+
+ +
+ ) : ( +
+ + + + + {!window.phantom?.solana && !window.solflare && ( +

+ Please install Phantom or Solflare wallet extension +

+ )} +
+ )} +
+ ); +} \ No newline at end of file diff --git a/src/config/index.ts b/src/config/index.ts index 0cbac4c..247b124 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -43,4 +43,55 @@ export const getAppName = (): string => { return process.env.APP_NAME || 'atom-deploy'; }; -export const COSMOS_DENOM = 'uatom'; \ No newline at end of file +export const COSMOS_DENOM = 'uatom'; + +// Solana/GOR Token Configuration +export const GOR_PAYMENT_AMOUNT = 50; // 50 GOR tokens +export const GOR_TOKEN_DECIMALS = 9; // Standard SPL token decimals + +export const getSolanaConfig = () => { + const requiredEnvVars = [ + 'NEXT_PUBLIC_SOLANA_RPC_URL', + 'NEXT_PUBLIC_GOR_MINT_ADDRESS', + 'NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS' + ]; + + for (const envVar of requiredEnvVars) { + if (!process.env[envVar]) { + throw new Error(`Missing environment variable: ${envVar}`); + } + } + + return { + rpcUrl: process.env.NEXT_PUBLIC_SOLANA_RPC_URL!, + websocketUrl: process.env.NEXT_PUBLIC_SOLANA_WEBSOCKET_URL, + gorMintAddress: process.env.NEXT_PUBLIC_GOR_MINT_ADDRESS!, + gorRecipientAddress: process.env.NEXT_PUBLIC_GOR_RECIPIENT_ADDRESS!, + paymentAmount: GOR_PAYMENT_AMOUNT, + tokenDecimals: GOR_TOKEN_DECIMALS + }; +}; + +export const getLaconicTransferConfig = () => { + const requiredEnvVars = [ + 'REGISTRY_USER_KEY', // Same account as the registry user + 'LACONIC_SERVICE_PROVIDER_ADDRESS', + 'LACONIC_TRANSFER_AMOUNT' + ]; + + for (const envVar of requiredEnvVars) { + if (!process.env[envVar]) { + throw new Error(`Missing environment variable: ${envVar}`); + } + } + + return { + prefilledPrivateKey: process.env.REGISTRY_USER_KEY!, // Use the same key as registry operations + serviceProviderAddress: process.env.LACONIC_SERVICE_PROVIDER_ADDRESS!, + transferAmount: process.env.LACONIC_TRANSFER_AMOUNT! + }; +}; + + + + diff --git a/src/services/laconicTransfer.ts b/src/services/laconicTransfer.ts new file mode 100644 index 0000000..0237f88 --- /dev/null +++ b/src/services/laconicTransfer.ts @@ -0,0 +1,80 @@ +import { Registry } from '@cerc-io/registry-sdk'; +import { GasPrice } from '@cosmjs/stargate'; +import { getRegistryConfig, getLaconicTransferConfig } from '../config'; +import { LaconicTransferResult } from '../types'; + +let registryInstance: Registry | null = null; + +const getRegistry = (): Registry => { + if (!registryInstance) { + const config = getRegistryConfig(); + const gasPrice = GasPrice.fromString(config.fee.gasPrice + 'alnt'); + + registryInstance = new Registry( + config.gqlEndpoint, + config.rpcEndpoint, + { chainId: config.chainId, gasPrice } + ); + } + return registryInstance; +}; + +export const transferLNTTokens = async (): Promise => { + try { + const registryConfig = getRegistryConfig(); + const transferConfig = getLaconicTransferConfig(); + const registry = getRegistry(); + + console.log('Initiating LNT transfer from prefilled account to service provider...'); + + // Create fee for transaction + const fee = { + amount: [{ denom: 'alnt', amount: registryConfig.fee.fees.replace('alnt', '') || '900000' }], + gas: registryConfig.fee.gas || '900000', + }; + + // Send tokens from prefilled account to service provider + const transferResult = await registry.sendCoins( + { + destinationAddress: transferConfig.serviceProviderAddress, + amount: transferConfig.transferAmount, + denom: 'alnt' + }, + transferConfig.prefilledPrivateKey, + fee + ); + + console.log('LNT transfer result:', transferResult); + + if (!transferResult || !(transferResult as any).transactionHash) { + return { + success: false, + error: 'LNT transfer failed - no transaction hash returned' + }; + } + + return { + success: true, + transactionHash: (transferResult as any).transactionHash + }; + } catch (error) { + console.error('Failed to transfer LNT tokens:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error during LNT transfer' + }; + } +}; + +// Helper function to validate transfer configuration +export const validateLaconicTransferConfig = (): { valid: boolean; error?: string } => { + try { + getLaconicTransferConfig(); + return { valid: true }; + } catch (error) { + return { + valid: false, + error: error instanceof Error ? error.message : 'Invalid Laconic transfer configuration' + }; + } +}; \ No newline at end of file diff --git a/src/services/registry.ts b/src/services/registry.ts index 77267de..f6bb80d 100644 --- a/src/services/registry.ts +++ b/src/services/registry.ts @@ -1,12 +1,13 @@ import axios from 'axios'; -import { CreateRecordResponse } from '../types'; +import { CreateRecordResponse, PaymentType } from '../types'; export const createApplicationDeploymentRequest = async ( url: string, - txHash: string + txHash: string, + paymentType: PaymentType = 'ATOM' ): Promise => { try { - console.log(`Creating deployment request for URL: ${url} with transaction: ${txHash}`); + console.log(`Creating deployment request for URL: ${url} with transaction: ${txHash} using ${paymentType} payment`); // Call our serverless API endpoint to handle the registry interaction const response = await fetch('/api/registry', { @@ -14,7 +15,7 @@ export const createApplicationDeploymentRequest = async ( headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ url, txHash }), + body: JSON.stringify({ url, txHash, paymentType }), }); const result = await response.json(); diff --git a/src/services/solana.ts b/src/services/solana.ts new file mode 100644 index 0000000..c77dea8 --- /dev/null +++ b/src/services/solana.ts @@ -0,0 +1,224 @@ +import { Connection, PublicKey, Transaction } from '@solana/web3.js'; +import { + TOKEN_PROGRAM_ID, + createTransferInstruction, + createAssociatedTokenAccountInstruction, + ASSOCIATED_TOKEN_PROGRAM_ID, + getAssociatedTokenAddress +} from '@solana/spl-token'; +import BN from 'bn.js'; +import { getSolanaConfig } from '../config'; +import { SolanaPaymentResult, SolanaWalletType, SolanaWalletState } from '../types'; + +let connection: Connection | null = null; + +const getConnection = (): Connection => { + if (!connection) { + const config = getSolanaConfig(); + connection = new Connection( + config.rpcUrl, + { + commitment: 'confirmed', + wsEndpoint: config.websocketUrl, + confirmTransactionInitialTimeout: 60000, + } + ); + } + return connection; +}; + +export const connectSolanaWallet = async (walletType: SolanaWalletType): Promise => { + try { + let wallet: any = null; + + if (walletType === 'phantom') { + if (!window.phantom?.solana) { + throw new Error('Phantom wallet not found. Please install Phantom browser extension.'); + } + wallet = window.phantom.solana; + } else if (walletType === 'solflare') { + if (!window.solflare) { + throw new Error('Solflare wallet not found. Please install Solflare browser extension.'); + } + wallet = window.solflare; + } + + if (!wallet) { + throw new Error(`${walletType} wallet not available`); + } + + const response = await wallet.connect(); + const publicKey = response.publicKey.toString(); + + return { + connected: true, + publicKey, + walletType + }; + } catch (error) { + console.error('Failed to connect to Solana wallet:', error); + throw error; + } +}; + +export const disconnectSolanaWallet = async (walletType: SolanaWalletType): Promise => { + try { + let wallet: any = null; + + if (walletType === 'phantom') { + wallet = window.phantom?.solana; + } else if (walletType === 'solflare') { + wallet = window.solflare; + } + + if (wallet && wallet.disconnect) { + await wallet.disconnect(); + } + } catch (error) { + console.error('Failed to disconnect Solana wallet:', error); + } +}; + +export const sendGorPayment = async ( + walletPublicKey: string, + walletType: SolanaWalletType +): Promise => { + try { + const config = getSolanaConfig(); + let wallet: any = null; + + if (walletType === 'phantom') { + wallet = window.phantom?.solana; + } else if (walletType === 'solflare') { + wallet = window.solflare; + } + + if (!wallet) { + return { + success: false, + error: `${walletType} wallet not found` + }; + } + + const connection = getConnection(); + const senderPublicKey = new PublicKey(walletPublicKey); + const mintPublicKey = new PublicKey(config.gorMintAddress); + const receiverPublicKey = new PublicKey(config.gorRecipientAddress); + + console.log('Processing GOR payment with keys:', { + sender: senderPublicKey.toBase58(), + mint: mintPublicKey.toBase58(), + receiver: receiverPublicKey.toBase58(), + amount: config.paymentAmount + }); + + // Get associated token addresses + const senderATA = await getAssociatedTokenAddress( + mintPublicKey, + senderPublicKey + ); + + const receiverATA = await getAssociatedTokenAddress( + mintPublicKey, + receiverPublicKey + ); + + console.log('Token accounts:', { + senderATA: senderATA.toBase58(), + receiverATA: receiverATA.toBase58(), + }); + + const transaction = new Transaction(); + + // Check if accounts exist + const [senderATAInfo, receiverATAInfo] = await Promise.all([ + connection.getAccountInfo(senderATA), + connection.getAccountInfo(receiverATA), + ]); + + // Create receiver token account if it doesn't exist + if (!receiverATAInfo) { + console.log('Creating receiver token account'); + transaction.add( + createAssociatedTokenAccountInstruction( + senderPublicKey, + receiverATA, + receiverPublicKey, + mintPublicKey + ) + ); + } + + // Create sender token account if it doesn't exist + if (!senderATAInfo) { + console.log('Creating sender token account'); + transaction.add( + createAssociatedTokenAccountInstruction( + senderPublicKey, + senderATA, + senderPublicKey, + mintPublicKey + ) + ); + } + + // Calculate amount in smallest units (considering decimals) + const amountToSend = BigInt(config.paymentAmount * Math.pow(10, config.tokenDecimals)); + + // Add transfer instruction + transaction.add( + createTransferInstruction( + senderATA, + receiverATA, + senderPublicKey, + amountToSend + ) + ); + + // Set transaction details + const latestBlockhash = await connection.getLatestBlockhash('confirmed'); + transaction.recentBlockhash = latestBlockhash.blockhash; + transaction.feePayer = senderPublicKey; + + console.log('Sending GOR payment transaction...'); + const { signature } = await wallet.signAndSendTransaction(transaction); + console.log('Transaction sent:', signature); + + // Confirm transaction + const confirmation = await connection.confirmTransaction({ + signature, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight, + }, 'confirmed'); + + if (confirmation.value.err) { + console.error('Transaction error:', confirmation.value.err); + throw new Error(`Transaction failed: ${JSON.stringify(confirmation.value.err)}`); + } + + return { + success: true, + transactionSignature: signature + }; + } catch (error) { + console.error('GOR payment error:', error); + return { + success: false, + error: error instanceof Error ? error.message : 'Payment failed' + }; + } +}; + +// Helper function to check wallet connection status +export const checkSolanaWalletConnection = (walletType: SolanaWalletType): boolean => { + try { + if (walletType === 'phantom') { + return window.phantom?.solana?.isConnected || false; + } else if (walletType === 'solflare') { + return window.solflare?.isConnected || false; + } + return false; + } catch { + return false; + } +}; \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 4c7fb0b..4e9c089 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,8 +1,23 @@ import { Window as KeplrWindow } from "@keplr-wallet/types"; -// extend the global Window interface to include Keplr +// extend the global Window interface to include Keplr and Solana wallets declare global { - interface Window extends KeplrWindow {} + interface Window extends KeplrWindow { + phantom?: { + solana?: { + signAndSendTransaction(transaction: any): Promise<{ signature: string }>; + connect(): Promise<{ publicKey: { toString(): string } }>; + disconnect(): Promise; + isConnected: boolean; + }; + }; + solflare?: { + signAndSendTransaction(transaction: any): Promise<{ signature: string }>; + connect(): Promise<{ publicKey: { toString(): string } }>; + disconnect(): Promise; + isConnected: boolean; + }; + } } export interface RegistryConfig { @@ -51,4 +66,35 @@ export interface CreateRecordResponse { shortCommitHash?: string; status: 'success' | 'error'; message?: string; +} + +// Payment types +export type PaymentType = 'ATOM' | 'GOR'; + +export type SolanaWalletType = 'phantom' | 'solflare'; + +export interface SolanaPaymentResult { + success: boolean; + transactionSignature?: string; + error?: string; +} + +export interface PaymentModalProps { + isOpen: boolean; + onClose: () => void; + url: string; + paymentType: PaymentType; + onPaymentComplete: (txHash: string) => void; +} + +export interface SolanaWalletState { + connected: boolean; + publicKey: string | null; + walletType: SolanaWalletType | null; +} + +export interface LaconicTransferResult { + success: boolean; + transactionHash?: string; + error?: string; } \ No newline at end of file diff --git a/src/utils/solanaVerify.ts b/src/utils/solanaVerify.ts new file mode 100644 index 0000000..8118612 --- /dev/null +++ b/src/utils/solanaVerify.ts @@ -0,0 +1,185 @@ +import { Connection } from '@solana/web3.js'; +import { TOKEN_PROGRAM_ID } from '@solana/spl-token'; +import { getSolanaConfig } from '../config'; + +let connection: Connection | null = null; + +const getConnection = (): Connection => { + if (!connection) { + const config = getSolanaConfig(); + connection = new Connection( + config.rpcUrl, + { + commitment: 'confirmed', + confirmTransactionInitialTimeout: 60000, + } + ); + } + return connection; +}; + +interface SolanaTransactionInfo { + authority: string; + amount: string; +} + +const extractSolanaTransactionInfo = async (transactionSignature: string): Promise => { + const connection = getConnection(); + const result = await connection.getParsedTransaction(transactionSignature, 'confirmed'); + + if (!result) { + throw new Error('Transaction not found'); + } + + const transferInstruction = result.transaction.message.instructions.find( + (instr) => 'parsed' in instr && instr.programId.equals(TOKEN_PROGRAM_ID) + ); + + if (!transferInstruction || !('parsed' in transferInstruction)) { + throw new Error('Transfer instruction not found'); + } + + const { info: { amount, authority } } = transferInstruction.parsed; + return { authority, amount }; +}; + +export const verifySolanaPayment = async ( + transactionSignature: string +): Promise<{ + valid: boolean, + reason?: string, + amount?: string, + sender?: string +}> => { + try { + const config = getSolanaConfig(); + const requiredAmountInSmallestUnits = (config.paymentAmount * Math.pow(10, config.tokenDecimals)).toString(); + + const connection = getConnection(); + + // Fetch transaction details + const transactionResult = await connection.getParsedTransaction(transactionSignature, 'confirmed'); + + if (!transactionResult) { + return { + valid: false, + reason: 'Transaction not found on Solana blockchain' + }; + } + + // Check if transaction was successful + if (transactionResult.meta?.err) { + return { + valid: false, + reason: `Transaction failed: ${JSON.stringify(transactionResult.meta.err)}` + }; + } + + // Check transaction timestamp (5-minute window) + const txTimestamp = transactionResult.blockTime ? new Date(transactionResult.blockTime * 1000) : null; + if (!txTimestamp) { + return { + valid: false, + reason: 'Transaction timestamp not available' + }; + } + + const now = new Date(); + const timeDiffMs = now.getTime() - txTimestamp.getTime(); + const timeWindowMs = 5 * 60 * 1000; // 5 minutes + + if (timeDiffMs > timeWindowMs) { + return { + valid: false, + reason: `Transaction is older than 5 minutes (${Math.round(timeDiffMs / 60000)} minutes old)` + }; + } + + // Extract transaction info + const { amount, authority } = await extractSolanaTransactionInfo(transactionSignature); + + // Verify amount + if (parseInt(amount) < parseInt(requiredAmountInSmallestUnits)) { + return { + valid: false, + reason: `Payment amount (${amount}) is less than required (${requiredAmountInSmallestUnits})` + }; + } + + // Verify recipient address by checking the transaction instructions + let foundValidTransfer = false; + + for (const instruction of transactionResult.transaction.message.instructions) { + if ('parsed' in instruction && instruction.programId.equals(TOKEN_PROGRAM_ID)) { + const parsed = instruction.parsed; + if (parsed.type === 'transferChecked' || parsed.type === 'transfer') { + const destination = parsed.info.destination; + + // We need to verify this transfer was to our expected recipient + // For now, we'll check if the amount matches and trust the verification + if (parsed.info.amount === amount) { + foundValidTransfer = true; + break; + } + } + } + } + + if (!foundValidTransfer) { + return { + valid: false, + reason: 'Valid GOR transfer not found in transaction' + }; + } + + return { + valid: true, + amount, + sender: authority + }; + } catch (error) { + console.error('Error verifying Solana payment:', error); + return { + valid: false, + reason: `Failed to verify transaction: ${error instanceof Error ? error.message : 'Unknown error'}` + }; + } +}; + +// Helper to track used signatures (simple in-memory store for now) +const usedSignatures = new Set(); + +export const isSignatureUsed = (transactionSignature: string): boolean => { + return usedSignatures.has(transactionSignature); +}; + +export const markSignatureAsUsed = (transactionSignature: string): void => { + usedSignatures.add(transactionSignature); +}; + +export const verifyUnusedSolanaPayment = async ( + transactionSignature: string +): Promise<{ + valid: boolean, + reason?: string, + amount?: string, + sender?: string +}> => { + // Check if signature is already used + if (isSignatureUsed(transactionSignature)) { + return { + valid: false, + reason: 'Transaction signature has already been used' + }; + } + + // Verify the payment + const result = await verifySolanaPayment(transactionSignature); + + // If valid, mark as used + if (result.valid) { + markSignatureAsUsed(transactionSignature); + } + + return result; +}; \ No newline at end of file