From 5f9ac10b68254782f268c9fe69e995051c672766 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 8 Aug 2024 18:49:22 +0200 Subject: [PATCH] Sign transactions early and dump them to spreadsheet (#79) * Replace `jest` with `vitest` for testing * Create test for getting route from squidrouter * Use vitest dependencies * Remove all references to jest * Add usdce and allow to pass fromToken to getRoute * Add refactoring * Add spreadsheet packages * Add env variables for google credentials * Implement spreadsheet storage * Upgrade vite version * Increase vite test timeout * Refactor tests * Add small changes * Improve handling for missing credentials in unit tests * Refactor code * Amend merge * Update api-solang package * Create GlobalSpreadsheet * Add phase 'prepareTransactions' * Split nabla extrinsic creation and submission * Bump api-solang package * Fix spreadsheet creation * Split functions for transactions into creation and submission * Add phase to prepare transactions * Fix tests * Fix some lint errors * Move storage logic to backend * Remove mutex * Increase timebounds of Stellar transactions * Move nabla code before swap again * Remove unused vars and config * Turn off mockSep24 --- package.json | 2 +- signer-service/package.json | 8 +- .../src/api/controllers/storage.controller.js | 40 + .../src/api/middlewares/validators.js | 17 +- signer-service/src/api/routes/v1/index.js | 7 +- .../src/api/routes/v1/storage.route.js | 9 + .../src/api/services/spreadsheet.service.js | 59 + signer-service/src/config/vars.js | 7 + signer-service/yarn.lock | 1029 ++++++++++++++++- src/services/nabla.ts | 294 +++-- src/services/offrampingFlow.ts | 39 +- .../polkadot/__tests__/spacewalk.test.tsx | 6 +- src/services/polkadot/index.tsx | 199 ++-- src/services/polkadot/polkadotApi.tsx | 30 +- src/services/polkadot/spacewalk.tsx | 45 +- src/services/signedTransactions.ts | 85 ++ src/services/stellar/index.tsx | 20 +- src/services/stellar/utils.tsx | 2 +- src/services/storage/remote.ts | 30 + src/wagmiConfig.ts | 2 +- vite.config.ts | 1 + yarn.lock | 10 +- 22 files changed, 1678 insertions(+), 263 deletions(-) create mode 100644 signer-service/src/api/controllers/storage.controller.js create mode 100644 signer-service/src/api/routes/v1/storage.route.js create mode 100644 signer-service/src/api/services/spreadsheet.service.js create mode 100644 src/services/signedTransactions.ts create mode 100644 src/services/storage/remote.ts diff --git a/package.json b/package.json index 7958d3d5..071f7ed0 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@mui/icons-material": "^5.14.19", "@mui/material": "^5.14.20", "@pendulum-chain/api": "^0.3.1", - "@pendulum-chain/api-solang": "^0.4.0", + "@pendulum-chain/api-solang": "^0.6.0", "@polkadot/api": "^9.9.1", "@polkadot/api-base": "^9.9.1", "@polkadot/api-contract": "^9.9.1", diff --git a/signer-service/package.json b/signer-service/package.json index fbc1c3ad..255b3c18 100644 --- a/signer-service/package.json +++ b/signer-service/package.json @@ -18,8 +18,7 @@ "lint": "eslint ./src/ --ignore-path .gitignore --ignore-pattern internals/scripts", "lint:fix": "yarn lint --fix", "lint:watch": "yarn lint --watch", - "test": "echo \"No tests defined yet\" && exit 0", - "test:cov": "echo \"No tests defined yet\" && exit 0", + "test": "vitest", "validate": "yarn lint && yarn test", "postpublish": "git push --tags", "docs": "apidoc -i src -o docs", @@ -38,6 +37,8 @@ "express": "^4.15.2", "express-rate-limit": "^6.7.0", "express-validation": "^1.0.2", + "google-auth-library": "^9.11.0", + "google-spreadsheet": "^4.1.2", "helmet": "^4.6.0", "http-status": "^1.0.1", "joi": "^10.4.1", @@ -74,6 +75,7 @@ "prettier": "^2.8.7", "sinon": "^7.5.0", "sinon-chai": "^3.0.0", - "supertest": "^6.1.3" + "supertest": "^6.1.3", + "vitest": "^2.0.5" } } diff --git a/signer-service/src/api/controllers/storage.controller.js b/signer-service/src/api/controllers/storage.controller.js new file mode 100644 index 00000000..f94b0bea --- /dev/null +++ b/signer-service/src/api/controllers/storage.controller.js @@ -0,0 +1,40 @@ +require('dotenv').config(); + +const { spreadsheet } = require('../../config/vars'); +const { initGoogleSpreadsheet, getOrCreateSheet, appendData } = require('../services/spreadsheet.service'); + +// These are the headers for the Google Spreadsheet +exports.SHEET_HEADER_VALUES = [ + 'timestamp', + 'polygonAddress', + 'stellarEphemeralPublicKey', + 'pendulumEphemeralPublicKey', + 'nablaApprovalTx', + 'nablaSwapTx', + 'spacewalkRedeemTx', + 'stellarOfframpTx', + 'stellarCleanupTx', +]; + +exports.storeData = async (req, res, next) => { + try { + // We expect the data to be an object that matches our schema + const data = req.body; + + // Try dumping transactions to spreadsheet + const sheet = await initGoogleSpreadsheet(spreadsheet.sheetId, spreadsheet.googleCredentials).then((doc) => { + return getOrCreateSheet(doc, this.SHEET_HEADER_VALUES); + }); + + if (sheet) { + console.log('Appending data to sheet'); + await appendData(sheet, data); + return res.status(200).json({ message: 'Data stored successfully' }); + } + + return res.status(500).json({ error: 'Failed to store data. Sheet unavailable.', details: error.message }); + } catch (error) { + console.error('Error in storeData:', error); + return res.status(500).json({ error: 'Failed to store data', details: error.message }); + } +}; diff --git a/signer-service/src/api/middlewares/validators.js b/signer-service/src/api/middlewares/validators.js index 4eb6c815..e8943da4 100644 --- a/signer-service/src/api/middlewares/validators.js +++ b/signer-service/src/api/middlewares/validators.js @@ -1,3 +1,5 @@ +const { SHEET_HEADER_VALUES } = require('../controllers/storage.controller'); + const validateCreationInput = (req, res, next) => { const { accountId, maxTime, assetCode } = req.body; if (!accountId || !maxTime) { @@ -30,4 +32,17 @@ const validateChangeOpInput = (req, res, next) => { next(); }; -module.exports = { validateChangeOpInput, validateCreationInput }; +const validateStorageInput = (req, res, next) => { + const data = req.body; + // Check if the data contains values for all the headers + if (!SHEET_HEADER_VALUES.every((header) => data[header])) { + const missingItems = SHEET_HEADER_VALUES.filter((header) => !data[header]); + let errorMessage = 'Data does not match schema. Missing items: ' + missingItems.join(', '); + console.error(errorMessage); + return res.status(400).json({ error: errorMessage }); + } + + next(); +}; + +module.exports = { validateChangeOpInput, validateCreationInput, validateStorageInput }; diff --git a/signer-service/src/api/routes/v1/index.js b/signer-service/src/api/routes/v1/index.js index 9beabb1f..e85172e4 100644 --- a/signer-service/src/api/routes/v1/index.js +++ b/signer-service/src/api/routes/v1/index.js @@ -1,6 +1,6 @@ const express = require('express'); -const httpStatus = require('http-status'); const stellarRoutes = require('./stellar.route'); +const storageRoutes = require('./storage.route'); const router = express.Router({ mergeParams: true }); const { sendStatusWithPk } = require('../../controllers/stellar.controller'); @@ -21,4 +21,9 @@ router.get('/status', sendStatusWithPk); */ router.use('/stellar', stellarRoutes); +/** + * POST v1/storage + */ +router.use('/storage', storageRoutes); + module.exports = router; diff --git a/signer-service/src/api/routes/v1/storage.route.js b/signer-service/src/api/routes/v1/storage.route.js new file mode 100644 index 00000000..3cfa61b4 --- /dev/null +++ b/signer-service/src/api/routes/v1/storage.route.js @@ -0,0 +1,9 @@ +const express = require('express'); +const controller = require('../../controllers/storage.controller'); +const { validateStorageInput } = require('../../middlewares/validators'); + +const router = express.Router({ mergeParams: true }); + +router.route('/create').post(validateStorageInput, controller.storeData); + +module.exports = router; diff --git a/signer-service/src/api/services/spreadsheet.service.js b/signer-service/src/api/services/spreadsheet.service.js new file mode 100644 index 00000000..2f2b0daa --- /dev/null +++ b/signer-service/src/api/services/spreadsheet.service.js @@ -0,0 +1,59 @@ +const { GoogleSpreadsheet } = require('google-spreadsheet'); +const { JWT } = require('google-auth-library'); + +const SCOPES = ['https://www.googleapis.com/auth/spreadsheets']; + +// googleCredentials: { email: string, key: string }, +exports.initGoogleSpreadsheet = async (sheetId, googleCredentials) => { + // Initialize auth - see https://theoephraim.github.io/node-google-spreadsheet/#/guides/authentication + if (!googleCredentials.email || !googleCredentials.key) { + throw new Error('Missing some google credentials'); + } + + const serviceAccountAuth = new JWT({ + // env var values here are copied from service account credentials generated by google + // see "Authentication" section in docs for more info + email: googleCredentials.email, + key: googleCredentials.key, + scopes: SCOPES, + }); + + const doc = new GoogleSpreadsheet(sheetId, serviceAccountAuth); + try { + await doc.loadInfo(); + } catch (error) { + console.error(`Error loading Google Spreadsheet ${sheetId}:`, error); + throw error; + } + + return doc; +}; + +// doc: GoogleSpreadsheet, headerValues: string[] +exports.getOrCreateSheet = async (doc, headerValues) => { + let sheet = doc.sheetsByIndex[0]; + try { + await sheet.loadHeaderRow(); + const sheetHeaders = sheet.headerValues; + + // Compare the header values to the expected header values + if ( + sheetHeaders.length !== headerValues.length && + sheetHeaders.every((value, index) => value === headerValues[index]) + ) { + // Create a new sheet if the headers don't match + console.log('Creating new sheet'); + sheet = await doc.addSheet({ headerValues }); + } + } catch (error) { + // Assume the error is due to the sheet not having any rows + await sheet.setHeaderRow(headerValues); + } + + return sheet; +}; + +// sheet: GoogleSpreadsheetWorksheet, data: Record +exports.appendData = async (sheet, data) => { + await sheet.addRow(data); +}; diff --git a/signer-service/src/config/vars.js b/signer-service/src/config/vars.js index e6085bac..3aa64104 100644 --- a/signer-service/src/config/vars.js +++ b/signer-service/src/config/vars.js @@ -14,4 +14,11 @@ module.exports = { rateLimitWindowMinutes: process.env.RATE_LIMIT_WINDOW_MINUTES || 15, rateLimitNumberOfProxies: process.env.RATE_LIMIT_NUMBER_OF_PROXIES || 1, logs: process.env.NODE_ENV === 'production' ? 'combined' : 'dev', + spreadsheet: { + googleCredentials: { + email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL, + key: process.env.GOOGLE_PRIVATE_KEY?.split(String.raw`\n`).join('\n'), + }, + sheetId: process.env.GOOGLE_SPREADSHEET_ID, + }, }; diff --git a/signer-service/yarn.lock b/signer-service/yarn.lock index 51f53054..414833a6 100644 --- a/signer-service/yarn.lock +++ b/signer-service/yarn.lock @@ -5,7 +5,7 @@ __metadata: version: 8 cacheKey: 10 -"@ampproject/remapping@npm:^2.2.0": +"@ampproject/remapping@npm:^2.2.0, @ampproject/remapping@npm:^2.3.0": version: 2.3.0 resolution: "@ampproject/remapping@npm:2.3.0" dependencies: @@ -280,6 +280,167 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/aix-ppc64@npm:0.21.5" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm64@npm:0.21.5" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/android-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-arm@npm:0.21.5" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@esbuild/android-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/android-x64@npm:0.21.5" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/darwin-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-arm64@npm:0.21.5" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/darwin-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/darwin-x64@npm:0.21.5" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/freebsd-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-arm64@npm:0.21.5" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/freebsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/freebsd-x64@npm:0.21.5" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/linux-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm64@npm:0.21.5" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/linux-arm@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-arm@npm:0.21.5" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@esbuild/linux-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ia32@npm:0.21.5" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/linux-loong64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-loong64@npm:0.21.5" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + +"@esbuild/linux-mips64el@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-mips64el@npm:0.21.5" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + +"@esbuild/linux-ppc64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-ppc64@npm:0.21.5" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + +"@esbuild/linux-riscv64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-riscv64@npm:0.21.5" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + +"@esbuild/linux-s390x@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-s390x@npm:0.21.5" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + +"@esbuild/linux-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/linux-x64@npm:0.21.5" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/netbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/netbsd-x64@npm:0.21.5" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openbsd-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/openbsd-x64@npm:0.21.5" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/sunos-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/sunos-x64@npm:0.21.5" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/win32-arm64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-arm64@npm:0.21.5" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@esbuild/win32-ia32@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-ia32@npm:0.21.5" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@esbuild/win32-x64@npm:0.21.5": + version: 0.21.5 + resolution: "@esbuild/win32-x64@npm:0.21.5" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^0.4.3": version: 0.4.3 resolution: "@eslint/eslintrc@npm:0.4.3" @@ -390,7 +551,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0": version: 1.5.0 resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" checksum: 4ed6123217569a1484419ac53f6ea0d9f3b57e5b57ab30d7c267bdb27792a27eb0e4b08e84a2680aa55cc2f2b411ffd6ec3db01c44fdc6dc43aca4b55f8374fd @@ -488,6 +649,8 @@ __metadata: express: "npm:^4.15.2" express-rate-limit: "npm:^6.7.0" express-validation: "npm:^1.0.2" + google-auth-library: "npm:^9.11.0" + google-spreadsheet: "npm:^4.1.2" helmet: "npm:^4.6.0" http-status: "npm:^1.0.1" husky: "npm:^3.0.7" @@ -514,6 +677,7 @@ __metadata: stellar-sdk: "npm:^11.3.0" supertest: "npm:^6.1.3" uuid: "npm:^3.1.0" + vitest: "npm:^2.0.5" winston: "npm:^3.1.0" languageName: unknown linkType: soft @@ -1058,6 +1222,118 @@ __metadata: languageName: node linkType: hard +"@rollup/rollup-android-arm-eabi@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.20.0" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@rollup/rollup-android-arm64@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-android-arm64@npm:4.20.0" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-arm64@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-darwin-arm64@npm:4.20.0" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-darwin-x64@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-darwin-x64@npm:4.20.0" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-gnueabihf@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.20.0" + conditions: os=linux & cpu=arm & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm-musleabihf@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.20.0" + conditions: os=linux & cpu=arm & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-gnu@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.20.0" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-arm64-musl@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.20.0" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.20.0" + conditions: os=linux & cpu=ppc64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-riscv64-gnu@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.20.0" + conditions: os=linux & cpu=riscv64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-s390x-gnu@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.20.0" + conditions: os=linux & cpu=s390x & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-gnu@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.20.0" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"@rollup/rollup-linux-x64-musl@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.20.0" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"@rollup/rollup-win32-arm64-msvc@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.20.0" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"@rollup/rollup-win32-ia32-msvc@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.20.0" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"@rollup/rollup-win32-x64-msvc@npm:4.20.0": + version: 4.20.0 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.20.0" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@scure/base@npm:^1.1.1, @scure/base@npm:^1.1.5": version: 1.1.7 resolution: "@scure/base@npm:1.1.7" @@ -1225,6 +1501,13 @@ __metadata: languageName: node linkType: hard +"@types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": + version: 1.0.5 + resolution: "@types/estree@npm:1.0.5" + checksum: 7de6d928dd4010b0e20c6919e1a6c27b61f8d4567befa89252055fad503d587ecb9a1e3eab1b1901f923964d7019796db810b7fd6430acb26c32866d126fd408 + languageName: node + linkType: hard + "@types/joi@npm:^14.3.3": version: 14.3.4 resolution: "@types/joi@npm:14.3.4" @@ -1279,6 +1562,69 @@ __metadata: languageName: node linkType: hard +"@vitest/expect@npm:2.0.5": + version: 2.0.5 + resolution: "@vitest/expect@npm:2.0.5" + dependencies: + "@vitest/spy": "npm:2.0.5" + "@vitest/utils": "npm:2.0.5" + chai: "npm:^5.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: ca9a218f50254b2259fd16166b2d8c9ccc8ee2cc068905e6b3d6281da10967b1590cc7d34b5fa9d429297f97e740450233745583b4cc12272ff11705faf70a37 + languageName: node + linkType: hard + +"@vitest/pretty-format@npm:2.0.5, @vitest/pretty-format@npm:^2.0.5": + version: 2.0.5 + resolution: "@vitest/pretty-format@npm:2.0.5" + dependencies: + tinyrainbow: "npm:^1.2.0" + checksum: 70bf452dd0b8525e658795125b3f11110bd6baadfaa38c5bb91ca763bded35ec6dc80e27964ad4e91b91be6544d35e18ea7748c1997693988f975a7283c3e9a0 + languageName: node + linkType: hard + +"@vitest/runner@npm:2.0.5": + version: 2.0.5 + resolution: "@vitest/runner@npm:2.0.5" + dependencies: + "@vitest/utils": "npm:2.0.5" + pathe: "npm:^1.1.2" + checksum: 464449abb84b3c779e1c6d1bedfc9e7469240ba3ccc4b4fa884386d1752d6572b68b9a87440159d433f17f61aca4012ee3bb78a3718d0e2bc64d810e9fc574a5 + languageName: node + linkType: hard + +"@vitest/snapshot@npm:2.0.5": + version: 2.0.5 + resolution: "@vitest/snapshot@npm:2.0.5" + dependencies: + "@vitest/pretty-format": "npm:2.0.5" + magic-string: "npm:^0.30.10" + pathe: "npm:^1.1.2" + checksum: fb46bc65851d4c8dcbbf86279c4146d5e7c17ad0d1be97132dedd98565d37f70ac8b0bf51ead0c6707786ffb15652535398c14d4304fa2146b0393d3db26fdff + languageName: node + linkType: hard + +"@vitest/spy@npm:2.0.5": + version: 2.0.5 + resolution: "@vitest/spy@npm:2.0.5" + dependencies: + tinyspy: "npm:^3.0.0" + checksum: ed19f4c3bb4d3853241e8070979615138e24403ce4c137fa48c903b3af2c8b3ada2cc26aca9c1aa323bb314a457a8130a29acbb18dafd4e42737deefb2abf1ca + languageName: node + linkType: hard + +"@vitest/utils@npm:2.0.5": + version: 2.0.5 + resolution: "@vitest/utils@npm:2.0.5" + dependencies: + "@vitest/pretty-format": "npm:2.0.5" + estree-walker: "npm:^3.0.3" + loupe: "npm:^3.1.1" + tinyrainbow: "npm:^1.2.0" + checksum: d631d56d29c33bc8de631166b2b6691c470187a345469dfef7048befe6027e1c6ff9552f2ee11c8a247522c325c4a64bfcc73f8f0f0c525da39cb9f190f119f8 + languageName: node + linkType: hard + "abbrev@npm:^2.0.0": version: 2.0.0 resolution: "abbrev@npm:2.0.0" @@ -1731,6 +2077,13 @@ __metadata: languageName: node linkType: hard +"assertion-error@npm:^2.0.1": + version: 2.0.1 + resolution: "assertion-error@npm:2.0.1" + checksum: a0789dd882211b87116e81e2648ccb7f60340b34f19877dd020b39ebb4714e475eb943e14ba3e22201c221ef6645b7bfe10297e76b6ac95b48a9898c1211ce66 + languageName: node + linkType: hard + "ast-types@npm:^0.13.4": version: 0.13.4 resolution: "ast-types@npm:0.13.4" @@ -1800,6 +2153,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.4.0": + version: 1.7.3 + resolution: "axios@npm:1.7.3" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 7f92af205705a8fb4a9d35666b663729507657f252a1d39d83582590119941872d49078017cf992e32f47aa3b7317f5439f77be772a173dac2ae0fedd38f43ae + languageName: node + linkType: hard + "axios@npm:^1.6.8": version: 1.7.2 resolution: "axios@npm:1.7.2" @@ -1865,7 +2229,7 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.3.1": +"base64-js@npm:^1.3.0, base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 @@ -1904,7 +2268,7 @@ __metadata: languageName: node linkType: hard -"bignumber.js@npm:^9.1.2": +"bignumber.js@npm:^9.0.0, bignumber.js@npm:^9.1.2": version: 9.1.2 resolution: "bignumber.js@npm:9.1.2" checksum: d89b8800a987225d2c00dcbf8a69dc08e92aa0880157c851c287b307d31ceb2fc2acb0c62c3e3a3d42b6c5fcae9b004035f13eb4386e56d529d7edac18d5c9d8 @@ -2093,6 +2457,13 @@ __metadata: languageName: node linkType: hard +"cac@npm:^6.7.14": + version: 6.7.14 + resolution: "cac@npm:6.7.14" + checksum: 002769a0fbfc51c062acd2a59df465a2a947916b02ac50b56c69ec6c018ee99ac3e7f4dd7366334ea847f1ecacf4defaa61bcd2ac283db50156ce1f1d8c8ad42 + languageName: node + linkType: hard + "cacache@npm:^18.0.0": version: 18.0.4 resolution: "cacache@npm:18.0.4" @@ -2252,6 +2623,19 @@ __metadata: languageName: node linkType: hard +"chai@npm:^5.1.1": + version: 5.1.1 + resolution: "chai@npm:5.1.1" + dependencies: + assertion-error: "npm:^2.0.1" + check-error: "npm:^2.1.1" + deep-eql: "npm:^5.0.1" + loupe: "npm:^3.1.0" + pathval: "npm:^2.0.0" + checksum: ee67279a5613bd36dc1dc13660042429ae2f1dc5a9030a6abcf381345866dfb5bce7bc10b9d74c8de86b6f656489f654bbbef3f3361e06925591e6a00c72afff + languageName: node + linkType: hard + "chalk@npm:3.0.0, chalk@npm:~3.0.0": version: 3.0.0 resolution: "chalk@npm:3.0.0" @@ -2308,6 +2692,13 @@ __metadata: languageName: node linkType: hard +"check-error@npm:^2.1.1": + version: 2.1.1 + resolution: "check-error@npm:2.1.1" + checksum: d785ed17b1d4a4796b6e75c765a9a290098cf52ff9728ce0756e8ffd4293d2e419dd30c67200aee34202463b474306913f2fcfaf1890641026d9fc6966fea27a + languageName: node + linkType: hard + "cheerio@npm:^0.22.0": version: 0.22.0 resolution: "cheerio@npm:0.22.0" @@ -2916,6 +3307,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:^4.3.5": + version: 4.3.6 + resolution: "debug@npm:4.3.6" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: d3adb9af7d57a9e809a68f404490cf776122acca16e6359a2702c0f462e510e91f9765c07f707b8ab0d91e03bad57328f3256f5082631cefb5393d0394d50fb7 + languageName: node + linkType: hard + "decamelize-keys@npm:^1.0.0": version: 1.1.1 resolution: "decamelize-keys@npm:1.1.1" @@ -2942,6 +3345,13 @@ __metadata: languageName: node linkType: hard +"deep-eql@npm:^5.0.1": + version: 5.0.2 + resolution: "deep-eql@npm:5.0.2" + checksum: a529b81e2ef8821621d20a36959a0328873a3e49d393ad11f8efe8559f31239494c2eb889b80342808674c475802ba95b9d6c4c27641b9a029405104c1b59fcf + languageName: node + linkType: hard + "deep-extend@npm:^0.6.0": version: 0.6.0 resolution: "deep-extend@npm:0.6.0" @@ -3204,7 +3614,7 @@ __metadata: languageName: node linkType: hard -"ecdsa-sig-formatter@npm:1.0.11": +"ecdsa-sig-formatter@npm:1.0.11, ecdsa-sig-formatter@npm:^1.0.11": version: 1.0.11 resolution: "ecdsa-sig-formatter@npm:1.0.11" dependencies: @@ -3486,6 +3896,86 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:^0.21.3": + version: 0.21.5 + resolution: "esbuild@npm:0.21.5" + dependencies: + "@esbuild/aix-ppc64": "npm:0.21.5" + "@esbuild/android-arm": "npm:0.21.5" + "@esbuild/android-arm64": "npm:0.21.5" + "@esbuild/android-x64": "npm:0.21.5" + "@esbuild/darwin-arm64": "npm:0.21.5" + "@esbuild/darwin-x64": "npm:0.21.5" + "@esbuild/freebsd-arm64": "npm:0.21.5" + "@esbuild/freebsd-x64": "npm:0.21.5" + "@esbuild/linux-arm": "npm:0.21.5" + "@esbuild/linux-arm64": "npm:0.21.5" + "@esbuild/linux-ia32": "npm:0.21.5" + "@esbuild/linux-loong64": "npm:0.21.5" + "@esbuild/linux-mips64el": "npm:0.21.5" + "@esbuild/linux-ppc64": "npm:0.21.5" + "@esbuild/linux-riscv64": "npm:0.21.5" + "@esbuild/linux-s390x": "npm:0.21.5" + "@esbuild/linux-x64": "npm:0.21.5" + "@esbuild/netbsd-x64": "npm:0.21.5" + "@esbuild/openbsd-x64": "npm:0.21.5" + "@esbuild/sunos-x64": "npm:0.21.5" + "@esbuild/win32-arm64": "npm:0.21.5" + "@esbuild/win32-ia32": "npm:0.21.5" + "@esbuild/win32-x64": "npm:0.21.5" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: d2ff2ca84d30cce8e871517374d6c2290835380dc7cd413b2d49189ed170d45e407be14de2cb4794cf76f75cf89955c4714726ebd3de7444b3046f5cab23ab6b + languageName: node + linkType: hard + "escalade@npm:^3.1.2": version: 3.1.2 resolution: "escalade@npm:3.1.2" @@ -3743,6 +4233,15 @@ __metadata: languageName: node linkType: hard +"estree-walker@npm:^3.0.3": + version: 3.0.3 + resolution: "estree-walker@npm:3.0.3" + dependencies: + "@types/estree": "npm:^1.0.0" + checksum: a65728d5727b71de172c5df323385755a16c0fdab8234dc756c3854cfee343261ddfbb72a809a5660fac8c75d960bb3e21aa898c2d7e9b19bb298482ca58a3af + languageName: node + linkType: hard + "esutils@npm:^2.0.2": version: 2.0.3 resolution: "esutils@npm:2.0.3" @@ -3807,6 +4306,23 @@ __metadata: languageName: node linkType: hard +"execa@npm:^8.0.1": + version: 8.0.1 + resolution: "execa@npm:8.0.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^8.0.1" + human-signals: "npm:^5.0.0" + is-stream: "npm:^3.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^5.1.0" + onetime: "npm:^6.0.0" + signal-exit: "npm:^4.1.0" + strip-final-newline: "npm:^3.0.0" + checksum: d2ab5fe1e2bb92b9788864d0713f1fce9a07c4594e272c0c97bc18c90569897ab262e4ea58d27a694d288227a2e24f16f5e2575b44224ad9983b799dc7f1098d + languageName: node + linkType: hard + "exponential-backoff@npm:^3.1.1": version: 3.1.1 resolution: "exponential-backoff@npm:3.1.1" @@ -3874,7 +4390,7 @@ __metadata: languageName: node linkType: hard -"extend@npm:~3.0.2": +"extend@npm:^3.0.2, extend@npm:~3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2" checksum: 59e89e2dc798ec0f54b36d82f32a27d5f6472c53974f61ca098db5d4648430b725387b53449a34df38fd0392045434426b012f302b3cc049a6500ccf82877e4e @@ -4243,7 +4759,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:~2.3.2": +"fsevents@npm:~2.3.2, fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" dependencies: @@ -4253,7 +4769,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" dependencies: @@ -4295,6 +4811,29 @@ __metadata: languageName: node linkType: hard +"gaxios@npm:^6.0.0, gaxios@npm:^6.1.1": + version: 6.7.0 + resolution: "gaxios@npm:6.7.0" + dependencies: + extend: "npm:^3.0.2" + https-proxy-agent: "npm:^7.0.1" + is-stream: "npm:^2.0.0" + node-fetch: "npm:^2.6.9" + uuid: "npm:^10.0.0" + checksum: 7ec9a430f6e13730dd957bc5971c74c738e7fdc6f6146c85c8fc353c1d5d036c188e2c5acf1f3806c5e3034f987b4d83ef705642ec9b60b73888927ffd1424c3 + languageName: node + linkType: hard + +"gcp-metadata@npm:^6.1.0": + version: 6.1.0 + resolution: "gcp-metadata@npm:6.1.0" + dependencies: + gaxios: "npm:^6.0.0" + json-bigint: "npm:^1.0.0" + checksum: a0d12a9cb7499fdb9de0fff5406aa220310c1326b80056be8d9b747aae26414f99d14bd795c0ec52ef7d0473eef9d61bb657b8cd3d8186c8a84c4ddbff025fe9 + languageName: node + linkType: hard + "gensync@npm:^1.0.0-beta.2": version: 1.0.0-beta.2 resolution: "gensync@npm:1.0.0-beta.2" @@ -4361,6 +4900,13 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^8.0.1": + version: 8.0.1 + resolution: "get-stream@npm:8.0.1" + checksum: dde5511e2e65a48e9af80fea64aff11b4921b14b6e874c6f8294c50975095af08f41bfb0b680c887f28b566dd6ec2cb2f960f9d36a323359be324ce98b766e9e + languageName: node + linkType: hard + "get-symbol-description@npm:^1.0.2": version: 1.0.2 resolution: "get-symbol-description@npm:1.0.2" @@ -4486,6 +5032,35 @@ __metadata: languageName: node linkType: hard +"google-auth-library@npm:^9.11.0": + version: 9.13.0 + resolution: "google-auth-library@npm:9.13.0" + dependencies: + base64-js: "npm:^1.3.0" + ecdsa-sig-formatter: "npm:^1.0.11" + gaxios: "npm:^6.1.1" + gcp-metadata: "npm:^6.1.0" + gtoken: "npm:^7.0.0" + jws: "npm:^4.0.0" + checksum: 132616d66833ac6b19d69360ac955e595c90261ad2f329c7b433615f4e21f7d831c6498dcd271ef92ea3e051c1bc26eed60a10b37d50de8936c36ae4459d8bc7 + languageName: node + linkType: hard + +"google-spreadsheet@npm:^4.1.2": + version: 4.1.2 + resolution: "google-spreadsheet@npm:4.1.2" + dependencies: + axios: "npm:^1.4.0" + lodash: "npm:^4.17.21" + peerDependencies: + google-auth-library: ^8.8.0 || ^9.0.0 + peerDependenciesMeta: + google-auth-library: + optional: true + checksum: 7121b13050e622c1aec0595cda70210b3f354683a06dea6a7a73514bf0c8b34f38e30b5dd59e055687d7da0b466ae26d748a49fc75b612289f04c090ea62f207 + languageName: node + linkType: hard + "gopd@npm:^1.0.1": version: 1.0.1 resolution: "gopd@npm:1.0.1" @@ -4509,6 +5084,16 @@ __metadata: languageName: node linkType: hard +"gtoken@npm:^7.0.0": + version: 7.1.0 + resolution: "gtoken@npm:7.1.0" + dependencies: + gaxios: "npm:^6.0.0" + jws: "npm:^4.0.0" + checksum: 640392261e55c9242137a81a4af8feb053b57061762cedddcbb6a0d62c2314316161808ac2529eea67d06d69fdc56d82361af50f2d840a04a87ea29e124d7382 + languageName: node + linkType: hard + "handlebars@npm:^4.7.7": version: 4.7.8 resolution: "handlebars@npm:4.7.8" @@ -4758,6 +5343,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^5.0.0": + version: 5.0.0 + resolution: "human-signals@npm:5.0.0" + checksum: 30f8870d831cdcd2d6ec0486a7d35d49384996742052cee792854273fa9dd9e7d5db06bb7985d4953e337e10714e994e0302e90dc6848069171b05ec836d65b0 + languageName: node + linkType: hard + "husky@npm:^3.0.7": version: 3.1.0 resolution: "husky@npm:3.1.0" @@ -5178,6 +5770,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "is-stream@npm:3.0.0" + checksum: 172093fe99119ffd07611ab6d1bcccfe8bc4aa80d864b15f43e63e54b7abc71e779acd69afdb854c4e2a67fdc16ae710e370eda40088d1cfc956a50ed82d8f16 + languageName: node + linkType: hard + "is-string@npm:^1.0.5, is-string@npm:^1.0.7": version: 1.0.7 resolution: "is-string@npm:1.0.7" @@ -5474,6 +6073,15 @@ __metadata: languageName: node linkType: hard +"json-bigint@npm:^1.0.0": + version: 1.0.0 + resolution: "json-bigint@npm:1.0.0" + dependencies: + bignumber.js: "npm:^9.0.0" + checksum: cd3973b88e5706f8f89d2a9c9431f206ef385bd5c584db1b258891a5e6642507c32316b82745239088c697f5ddfe967351e1731f5789ba7855aed56ad5f70e1f + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -5638,6 +6246,17 @@ __metadata: languageName: node linkType: hard +"jwa@npm:^2.0.0": + version: 2.0.0 + resolution: "jwa@npm:2.0.0" + dependencies: + buffer-equal-constant-time: "npm:1.0.1" + ecdsa-sig-formatter: "npm:1.0.11" + safe-buffer: "npm:^5.0.1" + checksum: ab983f6685d99d13ddfbffef9b1c66309a536362a8412d49ba6e687d834a1240ce39290f30ac7dbe241e0ab6c76fee7ff795776ce534e11d148158c9b7193498 + languageName: node + linkType: hard + "jws@npm:^3.2.2": version: 3.2.2 resolution: "jws@npm:3.2.2" @@ -5648,6 +6267,16 @@ __metadata: languageName: node linkType: hard +"jws@npm:^4.0.0": + version: 4.0.0 + resolution: "jws@npm:4.0.0" + dependencies: + jwa: "npm:^2.0.0" + safe-buffer: "npm:^5.0.1" + checksum: 1d15f4cdea376c6bd6a81002bd2cb0bf3d51d83da8f0727947b5ba3e10cf366721b8c0d099bf8c1eb99eb036e2c55e5fd5efd378ccff75a2b4e0bd10002348b9 + languageName: node + linkType: hard + "jwt-simple@npm:0.5.6": version: 0.5.6 resolution: "jwt-simple@npm:0.5.6" @@ -5940,7 +6569,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.4": +"lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 @@ -6019,6 +6648,15 @@ __metadata: languageName: node linkType: hard +"loupe@npm:^3.1.0, loupe@npm:^3.1.1": + version: 3.1.1 + resolution: "loupe@npm:3.1.1" + dependencies: + get-func-name: "npm:^2.0.1" + checksum: 56d71d64c5af109aaf2b5343668ea5952eed468ed2ff837373810e417bf8331f14491c6e4d38e08ff84a29cb18906e06e58ba660c53bd00f2989e1873fa2f54c + languageName: node + linkType: hard + "lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": version: 10.4.3 resolution: "lru-cache@npm:10.4.3" @@ -6051,6 +6689,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.10": + version: 0.30.11 + resolution: "magic-string@npm:0.30.11" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.0" + checksum: b784d2240252f5b1e755d487354ada4c672cbca16f045144f7185a75b059210e5fcca7be7be03ef1bac2ca754c4428b21d36ae64a9057ba429916f06b8c54eb2 + languageName: node + linkType: hard + "make-dir@npm:^3.0.0, make-dir@npm:^3.0.2": version: 3.1.0 resolution: "make-dir@npm:3.1.0" @@ -6198,6 +6845,13 @@ __metadata: languageName: node linkType: hard +"merge-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "merge-stream@npm:2.0.0" + checksum: 6fa4dcc8d86629705cea944a4b88ef4cb0e07656ebf223fa287443256414283dd25d91c1cd84c77987f2aec5927af1a9db6085757cb43d90eb170ebf4b47f4f4 + languageName: node + linkType: hard + "messageformat-formatters@npm:^2.0.1": version: 2.0.1 resolution: "messageformat-formatters@npm:2.0.1" @@ -6292,6 +6946,13 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: 995dcece15ee29aa16e188de6633d43a3db4611bcf93620e7e62109ec41c79c0f34277165b8ce5e361205049766e371851264c21ac64ca35499acb5421c2ba56 + languageName: node + linkType: hard + "minimatch@npm:3.0.4": version: 3.0.4 resolution: "minimatch@npm:3.0.4" @@ -6655,6 +7316,15 @@ __metadata: languageName: node linkType: hard +"nanoid@npm:^3.3.7": + version: 3.3.7 + resolution: "nanoid@npm:3.3.7" + bin: + nanoid: bin/nanoid.cjs + checksum: ac1eb60f615b272bccb0e2b9cd933720dad30bf9708424f691b8113826bb91aca7e9d14ef5d9415a6ba15c266b37817256f58d8ce980c82b0ba3185352565679 + languageName: node + linkType: hard + "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -6744,6 +7414,20 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^2.6.9": + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" + dependencies: + whatwg-url: "npm:^5.0.0" + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: b24f8a3dc937f388192e59bcf9d0857d7b6940a2496f328381641cb616efccc9866e89ec43f2ec956bbd6c3d3ee05524ce77fe7b29ccd34692b3a16f237d6676 + languageName: node + linkType: hard + "node-fetch@npm:^3.3.2": version: 3.3.2 resolution: "node-fetch@npm:3.3.2" @@ -6868,6 +7552,15 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^5.1.0": + version: 5.3.0 + resolution: "npm-run-path@npm:5.3.0" + dependencies: + path-key: "npm:^4.0.0" + checksum: ae8e7a89da9594fb9c308f6555c73f618152340dcaae423e5fb3620026fefbec463618a8b761920382d666fa7a2d8d240b6fe320e8a6cdd54dc3687e2b659d25 + languageName: node + linkType: hard + "nssocket@npm:0.6.0": version: 0.6.0 resolution: "nssocket@npm:0.6.0" @@ -7079,6 +7772,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^6.0.0": + version: 6.0.0 + resolution: "onetime@npm:6.0.0" + dependencies: + mimic-fn: "npm:^4.0.0" + checksum: 0846ce78e440841335d4e9182ef69d5762e9f38aa7499b19f42ea1c4cd40f0b4446094c455c713f9adac3f4ae86f613bb5e30c99e52652764d06a89f709b3788 + languageName: node + linkType: hard + "open@npm:^6.3.0, open@npm:^6.4.0": version: 6.4.0 resolution: "open@npm:6.4.0" @@ -7387,6 +8089,13 @@ __metadata: languageName: node linkType: hard +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 8e6c314ae6d16b83e93032c61020129f6f4484590a777eed709c4a01b50e498822b00f76ceaf94bc64dbd90b327df56ceadce27da3d83393790f1219e07721d7 + languageName: node + linkType: hard + "path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -7429,6 +8138,13 @@ __metadata: languageName: node linkType: hard +"pathe@npm:^1.1.2": + version: 1.1.2 + resolution: "pathe@npm:1.1.2" + checksum: f201d796351bf7433d147b92c20eb154a4e0ea83512017bf4ec4e492a5d6e738fb45798be4259a61aa81270179fce11026f6ff0d3fa04173041de044defe9d80 + languageName: node + linkType: hard + "pathval@npm:^1.1.1": version: 1.1.1 resolution: "pathval@npm:1.1.1" @@ -7436,6 +8152,13 @@ __metadata: languageName: node linkType: hard +"pathval@npm:^2.0.0": + version: 2.0.0 + resolution: "pathval@npm:2.0.0" + checksum: b91575bf9cdf01757afd7b5e521eb8a0b874a49bc972d08e0047cfea0cd3c019f5614521d4bc83d2855e3fcc331db6817dfd533dd8f3d90b16bc76fad2450fc1 + languageName: node + linkType: hard + "pause@npm:0.0.1": version: 0.0.1 resolution: "pause@npm:0.0.1" @@ -7620,6 +8343,17 @@ __metadata: languageName: node linkType: hard +"postcss@npm:^8.4.39": + version: 8.4.41 + resolution: "postcss@npm:8.4.41" + dependencies: + nanoid: "npm:^3.3.7" + picocolors: "npm:^1.0.1" + source-map-js: "npm:^1.2.0" + checksum: 6e6176c2407eff60493ca60a706c6b7def20a722c3adda94ea1ece38345eb99964191336fd62b62652279cec6938e79e0b1e1d477142c8d3516e7a725a74ee37 + languageName: node + linkType: hard + "prelude-ls@npm:^1.2.1": version: 1.2.1 resolution: "prelude-ls@npm:1.2.1" @@ -8381,6 +9115,69 @@ __metadata: languageName: node linkType: hard +"rollup@npm:^4.13.0": + version: 4.20.0 + resolution: "rollup@npm:4.20.0" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.20.0" + "@rollup/rollup-android-arm64": "npm:4.20.0" + "@rollup/rollup-darwin-arm64": "npm:4.20.0" + "@rollup/rollup-darwin-x64": "npm:4.20.0" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.20.0" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.20.0" + "@rollup/rollup-linux-arm64-gnu": "npm:4.20.0" + "@rollup/rollup-linux-arm64-musl": "npm:4.20.0" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.20.0" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.20.0" + "@rollup/rollup-linux-s390x-gnu": "npm:4.20.0" + "@rollup/rollup-linux-x64-gnu": "npm:4.20.0" + "@rollup/rollup-linux-x64-musl": "npm:4.20.0" + "@rollup/rollup-win32-arm64-msvc": "npm:4.20.0" + "@rollup/rollup-win32-ia32-msvc": "npm:4.20.0" + "@rollup/rollup-win32-x64-msvc": "npm:4.20.0" + "@types/estree": "npm:1.0.5" + fsevents: "npm:~2.3.2" + dependenciesMeta: + "@rollup/rollup-android-arm-eabi": + optional: true + "@rollup/rollup-android-arm64": + optional: true + "@rollup/rollup-darwin-arm64": + optional: true + "@rollup/rollup-darwin-x64": + optional: true + "@rollup/rollup-linux-arm-gnueabihf": + optional: true + "@rollup/rollup-linux-arm-musleabihf": + optional: true + "@rollup/rollup-linux-arm64-gnu": + optional: true + "@rollup/rollup-linux-arm64-musl": + optional: true + "@rollup/rollup-linux-powerpc64le-gnu": + optional: true + "@rollup/rollup-linux-riscv64-gnu": + optional: true + "@rollup/rollup-linux-s390x-gnu": + optional: true + "@rollup/rollup-linux-x64-gnu": + optional: true + "@rollup/rollup-linux-x64-musl": + optional: true + "@rollup/rollup-win32-arm64-msvc": + optional: true + "@rollup/rollup-win32-ia32-msvc": + optional: true + "@rollup/rollup-win32-x64-msvc": + optional: true + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: 448bd835715aa0f78c6888314e31fb92f1b83325ef0ff861a5a322c2bc87d200b2b6c4acb9223fb669c27ae0c4b071003b6877eec1d3411174615a4057db8946 + languageName: node + linkType: hard + "run-node@npm:^1.0.0": version: 1.0.0 resolution: "run-node@npm:1.0.0" @@ -8688,6 +9485,13 @@ __metadata: languageName: node linkType: hard +"siginfo@npm:^2.0.0": + version: 2.0.0 + resolution: "siginfo@npm:2.0.0" + checksum: e93ff66c6531a079af8fb217240df01f980155b5dc408d2d7bebc398dd284e383eb318153bf8acd4db3c4fe799aa5b9a641e38b0ba3b1975700b1c89547ea4e7 + languageName: node + linkType: hard + "signal-exit@npm:^3.0.0, signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.3": version: 3.0.7 resolution: "signal-exit@npm:3.0.7" @@ -8695,7 +9499,7 @@ __metadata: languageName: node linkType: hard -"signal-exit@npm:^4.0.1": +"signal-exit@npm:^4.0.1, signal-exit@npm:^4.1.0": version: 4.1.0 resolution: "signal-exit@npm:4.1.0" checksum: c9fa63bbbd7431066174a48ba2dd9986dfd930c3a8b59de9c29d7b6854ec1c12a80d15310869ea5166d413b99f041bfa3dd80a7947bcd44ea8e6eb3ffeabfa1f @@ -8824,6 +9628,13 @@ __metadata: languageName: node linkType: hard +"source-map-js@npm:^1.2.0": + version: 1.2.0 + resolution: "source-map-js@npm:1.2.0" + checksum: 74f331cfd2d121c50790c8dd6d3c9de6be21926de80583b23b37029b0f37aefc3e019fa91f9a10a5e120c08135297e1ecf312d561459c45908cb1e0e365f49e5 + languageName: node + linkType: hard + "source-map-support@npm:0.5.21": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" @@ -8963,6 +9774,13 @@ __metadata: languageName: node linkType: hard +"stackback@npm:0.0.2": + version: 0.0.2 + resolution: "stackback@npm:0.0.2" + checksum: 2d4dc4e64e2db796de4a3c856d5943daccdfa3dd092e452a1ce059c81e9a9c29e0b9badba91b43ef0d5ff5c04ee62feb3bcc559a804e16faf447bac2d883aa99 + languageName: node + linkType: hard + "statuses@npm:2.0.1": version: 2.0.1 resolution: "statuses@npm:2.0.1" @@ -8970,6 +9788,13 @@ __metadata: languageName: node linkType: hard +"std-env@npm:^3.7.0": + version: 3.7.0 + resolution: "std-env@npm:3.7.0" + checksum: 6ee0cca1add3fd84656b0002cfbc5bfa20340389d9ba4720569840f1caa34bce74322aef4c93f046391583e50649d0cf81a5f8fe1d411e50b659571690a45f12 + languageName: node + linkType: hard + "stellar-sdk@npm:^11.3.0": version: 11.3.0 resolution: "stellar-sdk@npm:11.3.0" @@ -9137,6 +9962,13 @@ __metadata: languageName: node linkType: hard +"strip-final-newline@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-final-newline@npm:3.0.0" + checksum: 23ee263adfa2070cd0f23d1ac14e2ed2f000c9b44229aec9c799f1367ec001478469560abefd00c5c99ee6f0b31c137d53ec6029c53e9f32a93804e18c201050 + languageName: node + linkType: hard + "strip-indent@npm:^2.0.0": version: 2.0.0 resolution: "strip-indent@npm:2.0.0" @@ -9301,6 +10133,34 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^2.8.0": + version: 2.9.0 + resolution: "tinybench@npm:2.9.0" + checksum: cfa1e1418e91289219501703c4693c70708c91ffb7f040fd318d24aef419fb5a43e0c0160df9471499191968b2451d8da7f8087b08c3133c251c40d24aced06c + languageName: node + linkType: hard + +"tinypool@npm:^1.0.0": + version: 1.0.0 + resolution: "tinypool@npm:1.0.0" + checksum: 4041a9ae62200626dceedbf4e58589d067a203eadcb88588d5681369b9a3c68987de14ce220b32a7e4ebfabaaf51ab9fa69408a7758827b7873f8204cdc79aa1 + languageName: node + linkType: hard + +"tinyrainbow@npm:^1.2.0": + version: 1.2.0 + resolution: "tinyrainbow@npm:1.2.0" + checksum: 2924444db6804355e5ba2b6e586c7f77329d93abdd7257a069a0f4530dff9f16de484e80479094e3f39273462541b003a65ee3a6afc2d12555aa745132deba5d + languageName: node + linkType: hard + +"tinyspy@npm:^3.0.0": + version: 3.0.0 + resolution: "tinyspy@npm:3.0.0" + checksum: b5b686acff2b88de60ff8ecf89a2042320406aaeee2fba1828a7ea8a925fad3ed9f5e4d7a068154a9134473c472aa03da8ca92ee994bc57a741c5ede5fa7de4d + languageName: node + linkType: hard + "titleize@npm:^2.1.0": version: 2.1.0 resolution: "titleize@npm:2.1.0" @@ -9387,6 +10247,13 @@ __metadata: languageName: node linkType: hard +"tr46@npm:~0.0.3": + version: 0.0.3 + resolution: "tr46@npm:0.0.3" + checksum: 8f1f5aa6cb232f9e1bdc86f485f916b7aa38caee8a778b378ffec0b70d9307873f253f5cbadbe2955ece2ac5c83d0dc14a77513166ccd0a0c7fe197e21396695 + languageName: node + linkType: hard + "trim-newlines@npm:^2.0.0": version: 2.0.0 resolution: "trim-newlines@npm:2.0.0" @@ -9731,6 +10598,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^10.0.0": + version: 10.0.0 + resolution: "uuid@npm:10.0.0" + bin: + uuid: dist/bin/uuid + checksum: 35aa60614811a201ff90f8ca5e9ecb7076a75c3821e17f0f5ff72d44e36c2d35fcbc2ceee9c4ac7317f4cc41895da30e74f3885e30313bee48fda6338f250538 + languageName: node + linkType: hard + "uuid@npm:^3.1.0, uuid@npm:^3.3.2, uuid@npm:^3.3.3": version: 3.4.0 resolution: "uuid@npm:3.4.0" @@ -9791,6 +10667,110 @@ __metadata: languageName: node linkType: hard +"vite-node@npm:2.0.5": + version: 2.0.5 + resolution: "vite-node@npm:2.0.5" + dependencies: + cac: "npm:^6.7.14" + debug: "npm:^4.3.5" + pathe: "npm:^1.1.2" + tinyrainbow: "npm:^1.2.0" + vite: "npm:^5.0.0" + bin: + vite-node: vite-node.mjs + checksum: de259cdf4b9ff82f39ba92ffca99db8a80783efd2764d3553b62cd8c8864488d590114a75bc93a93bf5ba2a2086bea1bee4b0029da9e62c4c0d3bf6c1f364eed + languageName: node + linkType: hard + +"vite@npm:^5.0.0": + version: 5.3.5 + resolution: "vite@npm:5.3.5" + dependencies: + esbuild: "npm:^0.21.3" + fsevents: "npm:~2.3.3" + postcss: "npm:^8.4.39" + rollup: "npm:^4.13.0" + peerDependencies: + "@types/node": ^18.0.0 || >=20.0.0 + less: "*" + lightningcss: ^1.21.0 + sass: "*" + stylus: "*" + sugarss: "*" + terser: ^5.4.0 + dependenciesMeta: + fsevents: + optional: true + peerDependenciesMeta: + "@types/node": + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + bin: + vite: bin/vite.js + checksum: 5672dde4a969349d9cf90a9e43029c8489dfff60fb04d6a10717d6224553cf12283a8cace633fa80b006df6037f72d08a459a38bf8ea66cb19075d60fe159482 + languageName: node + linkType: hard + +"vitest@npm:^2.0.5": + version: 2.0.5 + resolution: "vitest@npm:2.0.5" + dependencies: + "@ampproject/remapping": "npm:^2.3.0" + "@vitest/expect": "npm:2.0.5" + "@vitest/pretty-format": "npm:^2.0.5" + "@vitest/runner": "npm:2.0.5" + "@vitest/snapshot": "npm:2.0.5" + "@vitest/spy": "npm:2.0.5" + "@vitest/utils": "npm:2.0.5" + chai: "npm:^5.1.1" + debug: "npm:^4.3.5" + execa: "npm:^8.0.1" + magic-string: "npm:^0.30.10" + pathe: "npm:^1.1.2" + std-env: "npm:^3.7.0" + tinybench: "npm:^2.8.0" + tinypool: "npm:^1.0.0" + tinyrainbow: "npm:^1.2.0" + vite: "npm:^5.0.0" + vite-node: "npm:2.0.5" + why-is-node-running: "npm:^2.3.0" + peerDependencies: + "@edge-runtime/vm": "*" + "@types/node": ^18.0.0 || >=20.0.0 + "@vitest/browser": 2.0.5 + "@vitest/ui": 2.0.5 + happy-dom: "*" + jsdom: "*" + peerDependenciesMeta: + "@edge-runtime/vm": + optional: true + "@types/node": + optional: true + "@vitest/browser": + optional: true + "@vitest/ui": + optional: true + happy-dom: + optional: true + jsdom: + optional: true + bin: + vitest: vitest.mjs + checksum: abb916e3496a3fa9e9d05ecd806332dc4000aa0e433f0cb1e99f9dd1fa5c06d2c66656874b9860a683cec0f32abe1519599babef02e5c0ca80e9afbcdbddfdbd + languageName: node + linkType: hard + "vizion@npm:~2.2.1": version: 2.2.1 resolution: "vizion@npm:2.2.1" @@ -9841,6 +10821,23 @@ __metadata: languageName: node linkType: hard +"webidl-conversions@npm:^3.0.0": + version: 3.0.1 + resolution: "webidl-conversions@npm:3.0.1" + checksum: b65b9f8d6854572a84a5c69615152b63371395f0c5dcd6729c45789052296df54314db2bc3e977df41705eacb8bc79c247cee139a63fa695192f95816ed528ad + languageName: node + linkType: hard + +"whatwg-url@npm:^5.0.0": + version: 5.0.0 + resolution: "whatwg-url@npm:5.0.0" + dependencies: + tr46: "npm:~0.0.3" + webidl-conversions: "npm:^3.0.0" + checksum: f95adbc1e80820828b45cc671d97da7cd5e4ef9deb426c31bcd5ab00dc7103042291613b3ef3caec0a2335ed09e0d5ed026c940755dbb6d404e2b27f940fdf07 + languageName: node + linkType: hard + "which-boxed-primitive@npm:^1.0.2": version: 1.0.2 resolution: "which-boxed-primitive@npm:1.0.2" @@ -9907,6 +10904,18 @@ __metadata: languageName: node linkType: hard +"why-is-node-running@npm:^2.3.0": + version: 2.3.0 + resolution: "why-is-node-running@npm:2.3.0" + dependencies: + siginfo: "npm:^2.0.0" + stackback: "npm:0.0.2" + bin: + why-is-node-running: cli.js + checksum: 0de6e6cd8f2f94a8b5ca44e84cf1751eadcac3ebedcdc6e5fbbe6c8011904afcbc1a2777c53496ec02ced7b81f2e7eda61e76bf8262a8bc3ceaa1f6040508051 + languageName: node + linkType: hard + "wide-align@npm:1.1.3": version: 1.1.3 resolution: "wide-align@npm:1.1.3" diff --git a/src/services/nabla.ts b/src/services/nabla.ts index fc3239b4..eac04c57 100644 --- a/src/services/nabla.ts +++ b/src/services/nabla.ts @@ -1,37 +1,86 @@ import { Abi } from '@polkadot/api-contract'; import Big from 'big.js'; -import { readMessage, ReadMessageResult, executeMessage, ExecuteMessageResult } from '@pendulum-chain/api-solang'; +import { + createExecuteMessageExtrinsic, + ExecuteMessageResult, + Extrinsic, + readMessage, + ReadMessageResult, + submitExtrinsic, +} from '@pendulum-chain/api-solang'; import { EventStatus } from '../components/GenericEvent'; import { getApiManagerInstance } from './polkadot/polkadotApi'; import { erc20WrapperAbi } from '../contracts/ERC20Wrapper'; import { routerAbi } from '../contracts/Router'; import { NABLA_ROUTER } from '../constants/constants'; -import { defaultReadLimits, multiplyByPowerOfTen, stringifyBigWithSignificantDecimals } from '../helpers/contracts'; -import { parseContractBalanceResponse } from '../helpers/contracts'; -import { INPUT_TOKEN_CONFIG, OUTPUT_TOKEN_CONFIG, getPendulumCurrencyId } from '../constants/tokenConfig'; -import { defaultWriteLimits, createWriteOptions } from '../helpers/contracts'; +import { + createWriteOptions, + defaultReadLimits, + defaultWriteLimits, + multiplyByPowerOfTen, + parseContractBalanceResponse, + stringifyBigWithSignificantDecimals, +} from '../helpers/contracts'; +import { getPendulumCurrencyId, INPUT_TOKEN_CONFIG, OUTPUT_TOKEN_CONFIG } from '../constants/tokenConfig'; import { ExecutionContext, OfframpingState } from './offrampingFlow'; import { Keyring } from '@polkadot/api'; +import { decodeSubmittableExtrinsic } from './signedTransactions'; -// Since this operation reads first from chain the current approval, there is no need to -// save any state for potential recovery. -export async function nablaApprove( +async function createAndSignApproveExtrinsic({ + api, + token, + spender, + amount, + contractAbi, + keypairEphemeral, + nonce = -1, +}: any) { + console.log('write', `call approve ${token} for ${spender} with amount ${amount} `); + + const { execution, result: readMessageResult } = await createExecuteMessageExtrinsic({ + abi: contractAbi, + api, + callerAddress: keypairEphemeral.address, + contractDeploymentAddress: token, + messageName: 'approve', + messageArguments: [spender, amount], + limits: { ...defaultWriteLimits, ...createWriteOptions(api) }, + gasLimitTolerancePercentage: 10, // Allow 3 fold gas tolerance + }); + + console.log('result', readMessageResult); + + if (execution.type === 'onlyRpc') { + throw Error("Couldn't create approve extrinsic. Can't execute only-RPC"); + } + + const { extrinsic } = execution; + + return extrinsic.signAsync(keypairEphemeral, { nonce }); +} + +export async function prepareNablaApproveTransaction( state: OfframpingState, { renderEvent }: ExecutionContext, -): Promise { - const { inputTokenType, inputAmountNabla, pendulumEphemeralSeed } = state; +): Promise { + const { inputTokenType, inputAmountNabla, pendulumEphemeralSeed, nablaApproveNonce } = state; // event attempting swap const inputToken = INPUT_TOKEN_CONFIG[inputTokenType]; - console.log('swap', 'Attempting swap', inputAmountNabla.units, inputTokenType); + console.log( + 'swap', + 'Preparing the signed extrinsic for the approval of swap', + inputAmountNabla.units, + inputTokenType, + ); // get chain api, abi const { ss58Format, api } = (await getApiManagerInstance()).apiData!; const erc20ContractAbi = new Abi(erc20WrapperAbi, api.registry.getChainProperties()); // get asset details - // get ephermal keypair and account + // get ephemeral keypair and account const keyring = new Keyring({ type: 'sr25519', ss58Format }); const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); @@ -61,13 +110,14 @@ export async function nablaApprove( `Approving tokens: ${inputAmountNabla.units} ${inputToken.axelarEquivalent.pendulumAssetSymbol}`, EventStatus.Waiting, ); - await approve({ + return createAndSignApproveExtrinsic({ api: api, amount: inputAmountNabla.raw, token: inputToken.axelarEquivalent.pendulumErc20WrapperAddress, spender: NABLA_ROUTER, contractAbi: erc20ContractAbi, keypairEphemeral: ephemeralKeypair, + nonce: nablaApproveNonce, }); } catch (e) { renderEvent(`Could not approve token: ${e}`, EventStatus.Error); @@ -75,14 +125,96 @@ export async function nablaApprove( } } + throw Error("Couldn't create approve extrinsic"); +} + +// Since this operation reads first from chain the current approval, there is no need to +// save any state for potential recovery. +export async function nablaApprove( + state: OfframpingState, + { renderEvent }: ExecutionContext, +): Promise { + const { transactions, inputAmountNabla, inputTokenType } = state; + const inputToken = INPUT_TOKEN_CONFIG[inputTokenType]; + + if (!transactions) { + console.error('Missing transactions for nablaApprove'); + return { ...state, phase: 'failure' }; + } + + try { + renderEvent( + `Approving tokens: ${inputAmountNabla.units} ${inputToken.axelarEquivalent.pendulumAssetSymbol}`, + EventStatus.Waiting, + ); + + const { api } = (await getApiManagerInstance()).apiData!; + + const approvalExtrinsic = decodeSubmittableExtrinsic(transactions.nablaApproveTransaction, api); + + const result = await submitExtrinsic(approvalExtrinsic); + + if (result.status.type === 'error') { + renderEvent(`Could not approve token: ${result.status.error.toString()}`, EventStatus.Error); + return Promise.reject('Could not approve token'); + } + } catch (e) { + let errorMessage = ''; + const result = (e as ExecuteMessageResult).result; + if (result?.type === 'reverted') { + errorMessage = result.description; + } else if (result?.type === 'error') { + errorMessage = result.error; + } else { + errorMessage = 'Something went wrong'; + } + renderEvent(`Could not approve the required amount of token: ${errorMessage}`, EventStatus.Error); + return Promise.reject('Could not approve token'); + } + return { ...state, phase: 'nablaSwap', }; } -export async function nablaSwap(state: OfframpingState, { renderEvent }: ExecutionContext): Promise { - const { inputTokenType, outputTokenType, inputAmountNabla, outputAmount, pendulumEphemeralSeed } = state; +export async function createAndSignSwapExtrinsic({ + api, + tokenIn, + tokenOut, + amount, + amountMin, + contractAbi, + keypairEphemeral, + nonce = -1, +}: any) { + const { execution } = await createExecuteMessageExtrinsic({ + abi: contractAbi, + api, + callerAddress: keypairEphemeral.address, + contractDeploymentAddress: NABLA_ROUTER, + messageName: 'swapExactTokensForTokens', + // Params found at https://github.com/0xamberhq/contracts/blob/e3ab9132dbe2d54a467bdae3fff20c13400f4d84/contracts/src/core/Router.sol#L98 + messageArguments: [amount, amountMin, [tokenIn, tokenOut], keypairEphemeral.address, calcDeadline(5)], + limits: { ...defaultWriteLimits, ...createWriteOptions(api) }, + gasLimitTolerancePercentage: 10, // Allow 3 fold gas tolerance + skipDryRunning: true, // We have to skip this because it will not work before the approval transaction executed + }); + + if (execution.type === 'onlyRpc') { + throw Error("Couldn't create swap extrinsic. Can't execute only-RPC"); + } + + const { extrinsic } = execution; + return extrinsic.signAsync(keypairEphemeral, { nonce }); +} + +export async function prepareNablaSwapTransaction( + state: OfframpingState, + { renderEvent }: ExecutionContext, +): Promise { + const { inputTokenType, outputTokenType, inputAmountNabla, outputAmount, pendulumEphemeralSeed, nablaSwapNonce } = + state; // event attempting swap const inputToken = INPUT_TOKEN_CONFIG[inputTokenType]; @@ -93,7 +225,7 @@ export async function nablaSwap(state: OfframpingState, { renderEvent }: Executi const routerAbiObject = new Abi(routerAbi, api.registry.getChainProperties()); // get asset details - // get ephermal keypair and account + // get ephemeral keypair and account const keyring = new Keyring({ type: 'sr25519', ss58Format }); const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); @@ -113,7 +245,7 @@ export async function nablaSwap(state: OfframpingState, { renderEvent }: Executi EventStatus.Waiting, ); - await doActualSwap({ + return createAndSignSwapExtrinsic({ api: api, amount: inputAmountNabla.raw, // toString can render exponential notation amountMin: outputAmount.raw, // toString can render exponential notation @@ -121,88 +253,82 @@ export async function nablaSwap(state: OfframpingState, { renderEvent }: Executi tokenOut: outputToken.erc20WrapperAddress, contractAbi: routerAbiObject, keypairEphemeral: ephemeralKeypair, + nonce: nablaSwapNonce, }); } catch (e) { - let errorMessage = ''; - const result = (e as ExecuteMessageResult).result; - if (result.type === 'reverted') { - errorMessage = result.description; - } else if (result.type === 'error') { - errorMessage = result.error; - } else { - errorMessage = 'Something went wrong'; - } - renderEvent(`Could not swap the required amount of token: ${errorMessage}`, EventStatus.Error); - return Promise.reject('Could not swap token'); + return Promise.reject('Could not create swap transaction' + e?.toString()); } + } - //verify token balance before releasing this process. - const responseBalanceAfter = (await api.query.tokens.accounts(ephemeralKeypair.address, outputCurrencyId)) as any; - const rawBalanceAfter = Big(responseBalanceAfter?.free?.toString() ?? '0'); - - const actualOfframpValueRaw = rawBalanceAfter.sub(rawBalanceBefore); + throw Error("Couldn't create swap extrinsic"); +} - const actualOfframpValue = multiplyByPowerOfTen(actualOfframpValueRaw, -outputToken.decimals); +export async function nablaSwap(state: OfframpingState, { renderEvent }: ExecutionContext): Promise { + const { transactions, inputAmountNabla, inputTokenType, outputAmount, outputTokenType, pendulumEphemeralSeed } = + state; + const inputToken = INPUT_TOKEN_CONFIG[inputTokenType]; + const outputToken = OUTPUT_TOKEN_CONFIG[outputTokenType]; - renderEvent( - `Swap successful. Amount received: ${stringifyBigWithSignificantDecimals(actualOfframpValue, 2)}`, - EventStatus.Success, - ); + if (transactions === undefined) { + console.error('Missing transactions for nablaSwap'); + return { ...state, phase: 'failure' }; } - return { - ...state, - phase: 'executeSpacewalkRedeem', - }; -} + const { api, ss58Format } = (await getApiManagerInstance()).apiData!; -async function approve({ api, token, spender, amount, contractAbi, keypairEphemeral }: any) { - console.log('write', `call approve ${token} for ${spender} with amount ${amount} `); + // get ephemeral keypair and account + const keyring = new Keyring({ type: 'sr25519', ss58Format }); + const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); + // balance before the swap. Important for recovery process. + // if transaction was able to get in, but we failed on the listening + const outputCurrencyId = getPendulumCurrencyId(outputTokenType); + const responseBalanceBefore = (await api.query.tokens.accounts(ephemeralKeypair.address, outputCurrencyId)) as any; + const rawBalanceBefore = Big(responseBalanceBefore?.free?.toString() ?? '0'); - const response = await executeMessage({ - abi: contractAbi, - api, - callerAddress: keypairEphemeral.address, - contractDeploymentAddress: token, - getSigner: () => - Promise.resolve({ - type: 'keypair', - keypair: keypairEphemeral, - }), - messageName: 'approve', - messageArguments: [spender, amount], - limits: { ...defaultWriteLimits, ...createWriteOptions(api) }, - gasLimitTolerancePercentage: 10, // Allow 3 fold gas tolerance - }); + try { + renderEvent( + `Swapping ${inputAmountNabla.units} ${inputToken.axelarEquivalent.pendulumAssetSymbol} to ${outputAmount.units} ${outputToken.stellarAsset.code.string} `, + EventStatus.Waiting, + ); - console.log('write', 'call approve response', keypairEphemeral.address, [spender, amount], response); + const swapExtrinsic = decodeSubmittableExtrinsic(transactions.nablaSwapTransaction, api); + const result = await submitExtrinsic(swapExtrinsic); - if (response?.result?.type !== 'success') throw response; - return response; -} + if (result.status.type === 'error') { + renderEvent(`Could not swap token: ${result.status.error.toString()}`, EventStatus.Error); + return Promise.reject('Could not swap token'); + } + } catch (e) { + let errorMessage = ''; + const result = (e as ExecuteMessageResult).result; + if (result?.type === 'reverted') { + errorMessage = result.description; + } else if (result?.type === 'error') { + errorMessage = result.error; + } else { + errorMessage = 'Something went wrong'; + } + renderEvent(`Could not swap the required amount of token: ${errorMessage}`, EventStatus.Error); + return Promise.reject('Could not swap token'); + } + //verify token balance before releasing this process. + const responseBalanceAfter = (await api.query.tokens.accounts(ephemeralKeypair.address, outputCurrencyId)) as any; + const rawBalanceAfter = Big(responseBalanceAfter?.free?.toString() ?? '0'); -async function doActualSwap({ api, tokenIn, tokenOut, amount, amountMin, contractAbi, keypairEphemeral }: any) { - console.log('write', `call swap ${tokenIn} for ${tokenOut} with amount ${amount}, minimum expexted ${amountMin} `); + const actualOfframpValueRaw = rawBalanceAfter.sub(rawBalanceBefore); + const actualOfframpValue = multiplyByPowerOfTen(actualOfframpValueRaw, -outputToken.decimals); - const response = await executeMessage({ - abi: contractAbi, - api, - callerAddress: keypairEphemeral.address, - contractDeploymentAddress: NABLA_ROUTER, - getSigner: () => - Promise.resolve({ - type: 'keypair', - keypair: keypairEphemeral, - }), - messageName: 'swapExactTokensForTokens', - // Params found at https://github.com/0xamberhq/contracts/blob/e3ab9132dbe2d54a467bdae3fff20c13400f4d84/contracts/src/core/Router.sol#L98 - messageArguments: [amount, amountMin, [tokenIn, tokenOut], keypairEphemeral.address, calcDeadline(5)], - limits: { ...defaultWriteLimits, ...createWriteOptions(api) }, - gasLimitTolerancePercentage: 10, // Allow 3 fold gas tolerance - }); + renderEvent( + `Swap successful. Amount received: ${stringifyBigWithSignificantDecimals(actualOfframpValue, 2)}`, + EventStatus.Success, + ); - if (response?.result?.type !== 'success') throw response; - return response; + console.log('Swap successful'); + + return { + ...state, + phase: 'executeSpacewalkRedeem', + }; } const calcDeadline = (min: number) => `${Math.floor(Date.now() / 1000) + min * 60}`; diff --git a/src/services/offrampingFlow.ts b/src/services/offrampingFlow.ts index 50aa408f..76c221da 100644 --- a/src/services/offrampingFlow.ts +++ b/src/services/offrampingFlow.ts @@ -1,20 +1,22 @@ import { Config } from 'wagmi'; +import { storageService } from './storage/local'; import { INPUT_TOKEN_CONFIG, InputTokenType, OUTPUT_TOKEN_CONFIG, OutputTokenType } from '../constants/tokenConfig'; import { squidRouter } from './squidrouter/process'; import { createPendulumEphemeralSeed, pendulumCleanup, pendulumFundEphemeral } from './polkadot/ephemeral'; -import { SepResult, createStellarEphemeralSecret } from './anchor'; +import { createStellarEphemeralSecret, SepResult } from './anchor'; import Big from 'big.js'; import { multiplyByPowerOfTen } from '../helpers/contracts'; -import { setUpAccountAndOperations, stellarCleanup, stellarCreateEphemeral, stellarOfframp } from './stellar'; +import { stellarCleanup, stellarOfframp } from './stellar'; import { nablaApprove, nablaSwap } from './nabla'; import { RenderEventHandler } from '../components/GenericEvent'; import { executeSpacewalkRedeem } from './polkadot'; import { fetchSigningServiceAccountId } from './signingService'; import { Keypair } from 'stellar-sdk'; -import { storageService } from './storage/local'; import { SigningPhase } from '../hooks/useMainProcess'; +import { prepareTransactions } from './signedTransactions'; export type OfframpingPhase = + | 'prepareTransactions' | 'squidRouter' | 'pendulumFundEphemeral' | 'nablaApprove' @@ -63,11 +65,16 @@ export interface OfframpingState { // executeSpacewalk executeSpacewalkNonce: number; - // stellarOfframp - stellarOfframpingTransaction: string; + sepResult: SepResult; - // stellarCleanup - stellarCleanupTransaction: string; + // All signed transactions, if available + transactions?: { + stellarOfframpingTransaction: string; + stellarCleanupTransaction: string; + spacewalkRedeemTransaction: string; + nablaApproveTransaction: string; + nablaSwapTransaction: string; + }; } export type StateTransitionFunction = ( @@ -76,6 +83,7 @@ export type StateTransitionFunction = ( ) => Promise; const STATE_ADVANCEMENT_HANDLERS: Record = { + prepareTransactions, squidRouter, pendulumFundEphemeral, nablaApprove, @@ -128,16 +136,6 @@ export async function constructInitialState({ const outputAmountBig = Big(amountOut).round(2, 0); const outputAmountRaw = multiplyByPowerOfTen(outputAmountBig, outputTokenDecimals).toFixed(); - await stellarCreateEphemeral(stellarEphemeralSecret, outputTokenType); - const stellarFundingAccountId = await fetchSigningServiceAccountId(); - const stellarEphemeralKeypair = Keypair.fromSecret(stellarEphemeralSecret); - const { offrampingTransaction, mergeAccountTransaction } = await setUpAccountAndOperations( - stellarFundingAccountId, - stellarEphemeralKeypair, - sepResult, - outputTokenType, - ); - const initialState: OfframpingState = { sep24Id, pendulumEphemeralSeed, @@ -156,13 +154,14 @@ export async function constructInitialState({ units: outputAmountBig.toFixed(2, 0), raw: outputAmountRaw, }, - phase: 'squidRouter', + phase: 'prepareTransactions', nablaApproveNonce: 0, nablaSwapNonce: 1, executeSpacewalkNonce: 2, - stellarOfframpingTransaction: offrampingTransaction.toEnvelope().toXDR().toString('base64'), - stellarCleanupTransaction: mergeAccountTransaction.toEnvelope().toXDR().toString('base64'), + sepResult, + + transactions: undefined, }; storageService.set(OFFRAMPING_STATE_LOCAL_STORAGE_KEY, initialState); diff --git a/src/services/polkadot/__tests__/spacewalk.test.tsx b/src/services/polkadot/__tests__/spacewalk.test.tsx index d0c537b2..8d4bf357 100644 --- a/src/services/polkadot/__tests__/spacewalk.test.tsx +++ b/src/services/polkadot/__tests__/spacewalk.test.tsx @@ -77,8 +77,10 @@ describe('VaultService', () => { const amount = await api.query.redeem.redeemMinimumTransferAmount(); const amountString = amount.toString(); - const redeem = vaultService.requestRedeem(keypair, amountString, stellarPkBytes); - expect(redeem).toBeInstanceOf(Promise); + const redeemRequest = await vaultService.createRequestRedeemExtrinsic(keypair, amountString, stellarPkBytes); + expect(redeemRequest).toBeInstanceOf(Promise); + + const redeem = await vaultService.submitRedeem(keypair.address, redeemRequest); const redeemRequestEvent = await redeem; expect(redeemRequestEvent).toBeDefined(); diff --git a/src/services/polkadot/index.tsx b/src/services/polkadot/index.tsx index 1c677980..bbb03d6e 100644 --- a/src/services/polkadot/index.tsx +++ b/src/services/polkadot/index.tsx @@ -1,7 +1,6 @@ import { Keypair } from 'stellar-sdk'; -import { ApiManager } from './polkadotApi'; -import { getVaultsForCurrency, VaultService } from './spacewalk'; -import { prettyPrintVaultId } from './spacewalk'; +import { ApiComponents, ApiManager } from './polkadotApi'; +import { getVaultsForCurrency, prettyPrintVaultId, VaultService } from './spacewalk'; import { EventListener } from './eventListener'; import { EventStatus } from '../../components/GenericEvent'; import { OUTPUT_TOKEN_CONFIG, OutputTokenDetails } from '../../constants/tokenConfig'; @@ -9,108 +8,146 @@ import { getStellarBalanceUnits } from '../stellar/utils'; import Big from 'big.js'; import { ExecutionContext, OfframpingState } from '../offrampingFlow'; import { Keyring } from '@polkadot/api'; +import { Extrinsic } from '@pendulum-chain/api-solang'; +import { decodeSubmittableExtrinsic } from '../signedTransactions'; -export async function executeSpacewalkRedeem( +async function createVaultService(apiComponents: ApiComponents, assetCodeHex: string, assetIssuerHex: string) { + const vaultsForCurrency = await getVaultsForCurrency(apiComponents.api, assetCodeHex, assetIssuerHex); + if (vaultsForCurrency.length === 0) { + throw new Error(`No vaults found for currency ${assetCodeHex}`); + } + const targetVaultId = vaultsForCurrency[0].id; + return new VaultService(targetVaultId, apiComponents); +} + +export async function prepareSpacewalkRedeemTransaction( state: OfframpingState, { renderEvent }: ExecutionContext, -): Promise { - const { outputAmount, stellarEphemeralSecret, pendulumEphemeralSeed, outputTokenType } = state; +): Promise { + const { outputAmount, stellarEphemeralSecret, pendulumEphemeralSeed, outputTokenType, executeSpacewalkNonce } = state; const outputToken = OUTPUT_TOKEN_CONFIG[outputTokenType]; const pendulumApiComponents = await new ApiManager().getApiComponents(); - const { ss58Format, api } = pendulumApiComponents; + const { ss58Format } = pendulumApiComponents; - // get ephermal keypair and account + // get ephemeral keypair and account const keyring = new Keyring({ type: 'sr25519', ss58Format }); const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); const stellarEphemeralKeypair = Keypair.fromSecret(stellarEphemeralSecret); const stellarTargetAccountId = stellarEphemeralKeypair.publicKey(); + // Generate raw public key for target + const stellarTargetKeypair = Keypair.fromPublicKey(stellarTargetAccountId); + const stellarTargetAccountIdRaw = stellarTargetKeypair.rawPublicKey(); + + try { + const vaultService = await createVaultService( + pendulumApiComponents, + outputToken.stellarAsset.code.hex, + outputToken.stellarAsset.issuer.hex, + ); + renderEvent( + `Requesting redeem of ${outputAmount.units} tokens for vault ${prettyPrintVaultId(vaultService.vaultId)}`, + EventStatus.Waiting, + ); + + return await vaultService.createRequestRedeemExtrinsic( + ephemeralKeypair, + outputAmount.raw, + stellarTargetAccountIdRaw, + executeSpacewalkNonce, + ); + } catch (e) { + console.error('Error in prepareSpacewalkRedeemTransaction: ', e); + } + throw Error("Couldn't create redeem extrinsic"); +} + +export async function executeSpacewalkRedeem( + state: OfframpingState, + { renderEvent }: ExecutionContext, +): Promise { + const { transactions, outputTokenType, outputAmount, pendulumEphemeralSeed, stellarEphemeralSecret } = state; + const outputToken = OUTPUT_TOKEN_CONFIG[outputTokenType]; + + if (!transactions) { + console.error('Transactions not prepared, cannot execute Spacewalk redeem'); + return { ...state, phase: 'failure' }; + } + let redeemRequestEvent; // We wait for up to 10 minutes const maxWaitingTimeMinutes = 10; const maxWaitingTimeMs = maxWaitingTimeMinutes * 60 * 1000; - const stellarPollingTimeMs = 1 * 1000; - - // One of these two values must exist - const vaultsForCurrency = await getVaultsForCurrency( - api, - outputToken.stellarAsset.code.hex, - outputToken.stellarAsset.issuer.hex, - ); - if (vaultsForCurrency.length === 0) { - throw new Error(`No vaults found for currency ${outputToken.stellarAsset.code.string}`); - } - const targetVaultId = vaultsForCurrency[0].id; - const vaultService = new VaultService(targetVaultId, pendulumApiComponents); + const stellarPollingTimeMs = 1000; - const amountUnitsBig = new Big(outputAmount.units); - // Generate raw public key for target - const stellarTargetKeypair = Keypair.fromPublicKey(stellarTargetAccountId); - const stellarTargetAccountIdRaw = stellarTargetKeypair.rawPublicKey(); + const pendulumApiComponents = await new ApiManager().getApiComponents(); + const { ss58Format, api } = pendulumApiComponents; - // Recovery guard. If the operation was shut before the redeem was executed (we didn't register the event) we can - // avoid sending it again. - // We check for stellar funds. - const someBalanceUnits = await getStellarBalanceUnits(stellarTargetAccountId, outputToken.stellarAsset.code.string); - if (someBalanceUnits.lt(amountUnitsBig)) { - let redeemRequestEvent; + // get ephemeral keypair and account + const keyring = new Keyring({ type: 'sr25519', ss58Format }); + const ephemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); - try { - renderEvent( - `Requesting redeem of ${outputAmount.units} tokens for vault ${prettyPrintVaultId(targetVaultId)}`, - EventStatus.Waiting, - ); - redeemRequestEvent = await vaultService.requestRedeem( - ephemeralKeypair, - outputAmount.raw, - stellarTargetAccountIdRaw, - ); - - console.log( - `Successfully posed redeem request ${redeemRequestEvent.redeemId} for vault ${prettyPrintVaultId( - targetVaultId, - )}`, - ); - //Render event that the extrinsic passed, and we are now waiting for the execution of it + try { + const vaultService = await createVaultService( + pendulumApiComponents, + outputToken.stellarAsset.code.hex, + outputToken.stellarAsset.issuer.hex, + ); + renderEvent( + `Requesting redeem of ${outputAmount.units} tokens for vault ${prettyPrintVaultId(vaultService.vaultId)}`, + EventStatus.Waiting, + ); + + const redeemExtrinsic = decodeSubmittableExtrinsic(transactions.spacewalkRedeemTransaction, api); + redeemRequestEvent = await vaultService.submitRedeem(ephemeralKeypair.address, redeemExtrinsic); + + console.log( + `Successfully posed redeem request ${redeemRequestEvent.redeemId} for vault ${prettyPrintVaultId( + vaultService.vaultId, + )}`, + ); + + // Render event that the extrinsic passed, and we are now waiting for the execution of it + renderEvent( + `Redeem request passed, waiting up to ${maxWaitingTimeMinutes} minutes for redeem execution event...`, + EventStatus.Waiting, + ); + try { const eventListener = EventListener.getEventListener(pendulumApiComponents.api); - - renderEvent( - `Redeem request passed, waiting up to ${maxWaitingTimeMinutes} minutes for redeem execution event...`, - EventStatus.Waiting, - ); + await eventListener.waitForRedeemExecuteEvent(redeemRequestEvent.redeemId, maxWaitingTimeMs); + } catch (error) { + // This is a potentially recoverable error (due to network delay) + // in the future we should distinguish between recoverable and non-recoverable errors + console.log(`Failed to wait for redeem execution: ${error}`); + renderEvent(`Failed to wait for redeem execution: Max waiting time exceeded`, EventStatus.Error); + throw new Error(`Failed to wait for redeem execution`); + } + } catch (error) { + // This is a potentially recoverable error (due to redeem request done before app shut down, but not registered) + if ((error as any).message.includes('AmountExceedsUserBalance')) { + console.log(`Recovery mode: Redeem already performed. Waiting for execution and Stellar balance arrival.`); + const amountUnitsBig = new Big(outputAmount.units); + const stellarEphemeralKeypair = Keypair.fromSecret(stellarEphemeralSecret); + const stellarTargetAccountId = stellarEphemeralKeypair.publicKey(); try { - await eventListener.waitForRedeemExecuteEvent(redeemRequestEvent.redeemId, maxWaitingTimeMs); - } catch (error) { - // This is a potentially recoverable error (due to network delay) - // in the future we should distinguish between recoverable and non-recoverable errors - console.log(`Failed to wait for redeem execution: ${error}`); - renderEvent(`Failed to wait for redeem execution: Max waiting time exceeded`, EventStatus.Error); - throw new Error(`Failed to wait for redeem execution`); - } - } catch (error) { - // This is a potentially recoverable error (due to redeem request done before app shut down, but not registered) - if ((error as any).message.includes('AmountExceedsUserBalance')) { - console.log(`Recovery mode: Redeem already performed. Waiting for execution and Stellar balance arrival.`); - try { - await checkBalancePeriodically( - stellarTargetAccountId, - outputToken, - amountUnitsBig, - stellarPollingTimeMs, - maxWaitingTimeMs, - ); - console.log('Balance check completed successfully.'); - } catch (balanceCheckError) { - throw new Error(`Stellar balance did not arrive on time`); - } - } else { - // Generic failure of the extrinsic itself OR lack of funds to even make the transaction - console.log(`Failed to request redeem: ${error}`); - throw new Error(`Failed to request redeem`); + await checkBalancePeriodically( + stellarTargetAccountId, + outputToken, + amountUnitsBig, + stellarPollingTimeMs, + maxWaitingTimeMs, + ); + console.log('Balance check completed successfully.'); + } catch (balanceCheckError) { + throw new Error(`Stellar balance did not arrive on time`); } + } else { + // Generic failure of the extrinsic itself OR lack of funds to even make the transaction + console.log(`Failed to request redeem: ${error}`); + throw new Error(`Failed to request redeem`); } } diff --git a/src/services/polkadot/polkadotApi.tsx b/src/services/polkadot/polkadotApi.tsx index 3d80a58d..1b63433d 100644 --- a/src/services/polkadot/polkadotApi.tsx +++ b/src/services/polkadot/polkadotApi.tsx @@ -4,7 +4,6 @@ const NETWORK = 'Pendulum'; export interface ApiComponents { api: ApiPromise; - mutex: Mutex; ss58Format: number; decimals: number; } @@ -18,13 +17,12 @@ class ApiManager { provider: wsProvider, noInitWarn: true, }); - const mutex = new Mutex(); const chainProperties = api.registry.getChainProperties(); const ss58Format = Number(chainProperties?.get('ss58Format')?.toString() ?? 42); const decimals = Number(chainProperties?.get('tokenDecimals')?.toHuman()[0]) ?? 12; - return { api, mutex, ss58Format, decimals }; + return { api, ss58Format, decimals }; } async populateApi() { @@ -45,35 +43,11 @@ class ApiManager { } } -class Mutex { - locks = new Map(); - - async lock(accountId: string) { - let resolveLock: (value: unknown) => void; - - const lockPromise = new Promise((resolve) => { - resolveLock = resolve; - }); - - const prevLock = this.locks.get(accountId) || Promise.resolve(); - this.locks.set( - accountId, - prevLock.then(() => lockPromise), - ); - - await prevLock; - - return () => { - resolveLock(undefined); - }; - } -} - let instance: ApiManager | undefined = undefined; export async function getApiManagerInstance(): Promise { if (!instance) { - let instancePreparing = new ApiManager(); + const instancePreparing = new ApiManager(); await instancePreparing.populateApi(); instance = instancePreparing; } diff --git a/src/services/polkadot/spacewalk.tsx b/src/services/polkadot/spacewalk.tsx index 18d990f2..4279bf51 100644 --- a/src/services/polkadot/spacewalk.tsx +++ b/src/services/polkadot/spacewalk.tsx @@ -10,6 +10,7 @@ import { WalletAccount } from '@talismn/connect-wallets'; import { getAddressForFormat } from '../../helpers/addressFormatter'; import { KeyringPair } from '@polkadot/keyring/types'; import { SpacewalkPrimitivesCurrencyId } from '@pendulum-chain/types/interfaces'; +import { SubmittableExtrinsic } from '@polkadot/api/types'; export function extractAssetFromWrapped(wrapped: SpacewalkPrimitivesCurrencyId) { if (!wrapped.isStellar) { @@ -104,7 +105,12 @@ export class VaultService { this.apiComponents = apiComponents; } - async requestRedeem(accountOrPair: WalletAccount | KeyringPair, amountRaw: string, stellarPkBytesBuffer: Buffer) { + async createRequestRedeemExtrinsic( + accountOrPair: WalletAccount | KeyringPair, + amountRaw: string, + stellarPkBytesBuffer: Buffer, + nonce = -1, + ) { const keyring = new Keyring({ type: 'sr25519' }); keyring.setSS58Format(this.apiComponents!.ss58Format); @@ -113,25 +119,27 @@ export class VaultService { const address = isWalletAccount(accountOrPair) ? accountOrPair.address : keyring.encodeAddress(accountOrPair.publicKey); - const options = isWalletAccount(accountOrPair) ? { signer: accountOrPair.signer as any } : {}; - - const release = await this.apiComponents!.mutex.lock(address); - const nonce = await this.apiComponents!.api.rpc.system.accountNextIndex(address); - console.log(`Nonce for ${getAddressForFormat(address, this.apiComponents!.ss58Format)} is ${nonce.toString()}`); + const options = isWalletAccount(accountOrPair) ? { signer: accountOrPair.signer as any, nonce } : { nonce }; const stellarPkBytes = Uint8Array.from(stellarPkBytesBuffer); - return new Promise((resolve, reject) => - this.apiComponents!.api.tx.redeem.requestRedeem(amountRaw, stellarPkBytes, this.vaultId!) - .signAndSend(addressOrPair, options, (submissionResult: ISubmittableResult) => { + return this.apiComponents!.api.tx.redeem.requestRedeem(amountRaw, stellarPkBytes, this.vaultId!).signAsync( + addressOrPair, + options, + ); + } + + async submitRedeem( + senderAddress: string, + extrinsic: SubmittableExtrinsic<'promise'>, + ): Promise { + return new Promise((resolve, reject) => { + extrinsic + .send((submissionResult: ISubmittableResult) => { const { status, events, dispatchError } = submissionResult; if (status.isFinalized) { - console.log( - `Requested redeem of ${amountRaw} for vault ${prettyPrintVaultId(this.vaultId)} with status ${ - status.type - }`, - ); + console.log(`Requested redeem for vault ${prettyPrintVaultId(this.vaultId)} with status ${status.type}`); // Try to find a 'system.ExtrinsicFailed' event const systemExtrinsicFailedEvent = events.find((record) => { @@ -151,11 +159,11 @@ export class VaultService { const event = redeemEvents .map((event) => parseEventRedeemRequest(event)) .filter((event) => { - return event.redeemer === getAddressForFormat(accountOrPair.address, this.apiComponents!.ss58Format); + return event.redeemer === getAddressForFormat(senderAddress, this.apiComponents!.ss58Format); }); if (event.length == 0) { - reject(new Error(`No redeem event found for account ${accountOrPair.address}`)); + reject(new Error(`No redeem event found for account ${senderAddress}`)); } //we should only find one event corresponding to the issue request if (event.length != 1) { @@ -166,9 +174,8 @@ export class VaultService { }) .catch((error) => { reject(new Error(`Failed to request redeem: ${error}`)); - }) - .finally(() => release()), - ); + }); + }); } // We first check if dispatchError is of type "module", diff --git a/src/services/signedTransactions.ts b/src/services/signedTransactions.ts new file mode 100644 index 00000000..cb6fa337 --- /dev/null +++ b/src/services/signedTransactions.ts @@ -0,0 +1,85 @@ +import { Extrinsic } from '@pendulum-chain/api-solang'; +import { ApiPromise, Keyring } from '@polkadot/api'; +import { prepareSpacewalkRedeemTransaction } from './polkadot'; +import { prepareNablaApproveTransaction, prepareNablaSwapTransaction } from './nabla'; +import { fetchSigningServiceAccountId } from './signingService'; +import { Keypair } from 'stellar-sdk'; +import { setUpAccountAndOperations, stellarCreateEphemeral } from './stellar'; +import { getApiManagerInstance } from './polkadot/polkadotApi'; +import { getAccount } from '@wagmi/core'; +import { ExecutionContext, OfframpingState } from './offrampingFlow'; +import { storeDataInBackend } from './storage/remote'; + +export function encodeSubmittableExtrinsic(extrinsic: Extrinsic) { + return extrinsic.toHex(); +} + +export function decodeSubmittableExtrinsic(encodedExtrinsic: string, api: ApiPromise) { + return api.tx(encodedExtrinsic); +} + +// Creates and signs all required transactions already so they are ready to be submitted. +// The transactions are stored in the state and the phase is updated to 'squidRouter'. +// The transactions are also dumped to a Google Spreadsheet. +export async function prepareTransactions(state: OfframpingState, context: ExecutionContext): Promise { + if (state.transactions !== undefined) { + console.error('Transactions already prepared'); + return state; + } + + const { stellarEphemeralSecret, pendulumEphemeralSeed, outputTokenType, sepResult } = state; + + const spacewalkRedeemTransaction = await prepareSpacewalkRedeemTransaction(state, context); + const nablaApproveTransaction = await prepareNablaApproveTransaction(state, context); + const nablaSwapTransaction = await prepareNablaSwapTransaction(state, context); + + // Fund Stellar ephemeral only after all other transactions are prepared + await stellarCreateEphemeral(stellarEphemeralSecret, outputTokenType); + const stellarFundingAccountId = await fetchSigningServiceAccountId(); + const stellarEphemeralKeypair = Keypair.fromSecret(stellarEphemeralSecret); + const stellarEphemeralPublicKey = stellarEphemeralKeypair.publicKey(); + const { offrampingTransaction, mergeAccountTransaction } = await setUpAccountAndOperations( + stellarFundingAccountId, + stellarEphemeralKeypair, + sepResult, + outputTokenType, + ); + + const transactions = { + stellarOfframpingTransaction: offrampingTransaction.toEnvelope().toXDR().toString('base64'), + stellarCleanupTransaction: mergeAccountTransaction.toEnvelope().toXDR().toString('base64'), + spacewalkRedeemTransaction: encodeSubmittableExtrinsic(spacewalkRedeemTransaction), + nablaSwapTransaction: encodeSubmittableExtrinsic(nablaSwapTransaction), + nablaApproveTransaction: encodeSubmittableExtrinsic(nablaApproveTransaction), + }; + + const apiManager = await getApiManagerInstance(); + const { ss58Format } = apiManager.apiData!; + const keyring = new Keyring({ type: 'sr25519', ss58Format }); + const pendulumEphemeralKeypair = keyring.addFromUri(pendulumEphemeralSeed); + const pendulumEphemeralPublicKey = pendulumEphemeralKeypair.address; + + // Get the Polygon account connected by the user + const polygonAccount = getAccount(context.wagmiConfig); + const polygonAddress = polygonAccount.address; + + // Try to store the data in the backend + try { + const data = { + timestamp: new Date().toISOString(), + polygonAddress: polygonAddress || '', + stellarEphemeralPublicKey, + pendulumEphemeralPublicKey, + nablaApprovalTx: transactions.nablaApproveTransaction, + nablaSwapTx: transactions.nablaSwapTransaction, + spacewalkRedeemTx: transactions.spacewalkRedeemTransaction, + stellarOfframpTx: transactions.stellarOfframpingTransaction, + stellarCleanupTx: transactions.stellarCleanupTransaction, + }; + await storeDataInBackend(data); + } catch (error) { + console.error('Error storing data', error); + } + + return { ...state, transactions, phase: 'squidRouter' }; +} diff --git a/src/services/stellar/index.tsx b/src/services/stellar/index.tsx index 13ef4de0..aea9cd2d 100644 --- a/src/services/stellar/index.tsx +++ b/src/services/stellar/index.tsx @@ -167,8 +167,8 @@ async function createOfframpAndMergeTransaction( ephemeralAccount: Account, { stellarAsset: { code, issuer } }: OutputTokenDetails, ) { - // We allow for more TTL since the redeem may take time - const maxTime = Date.now() + 1000 * 60 * 30; + // We allow for a TLL of up to two weeks so we are able to recover it in case of failure + const maxTime = Date.now() + 1000 * 60 * 60 * 24 * 14; const sequence = ephemeralAccount.sequenceNumber(); const { amount, memo, memoType, offrampingAccount } = sepResult; @@ -227,7 +227,7 @@ async function createOfframpAndMergeTransaction( .build(); // Fetch the signatures from the server - // Under this endpoint, it will return first first the signature of the offramp payment + // Under this endpoint, it will return first the signature of the offramp payment // with information provided, then the signature of the merge account operation // We also provide the ephemeral account's sequence number. This is more controlled @@ -266,8 +266,12 @@ async function createOfframpAndMergeTransaction( // if we are on recovery mode we can ignore this error. // Alternative improvement: check the balance of the destination (offramp) account to see if the funds arrived. export async function stellarOfframp(state: OfframpingState): Promise { + if (state.transactions === undefined) { + throw new Error('Transactions not prepared'); + } + try { - const offrampingTransaction = new Transaction(state.stellarOfframpingTransaction, NETWORK_PASSPHRASE); + const offrampingTransaction = new Transaction(state.transactions.stellarOfframpingTransaction, NETWORK_PASSPHRASE); await horizonServer.submitTransaction(offrampingTransaction); } catch (error) { const horizonError = error as { response: { data: { extras: any } } }; @@ -290,9 +294,13 @@ export async function stellarOfframp(state: OfframpingState): Promise { +): Promise { + if (state.transactions === undefined) { + throw new Error('Transactions not prepared'); + } + try { - const mergeAccountTransaction = new Transaction(state.stellarCleanupTransaction, NETWORK_PASSPHRASE); + const mergeAccountTransaction = new Transaction(state.transactions.stellarCleanupTransaction, NETWORK_PASSPHRASE); await horizonServer.submitTransaction(mergeAccountTransaction); } catch (error) { const horizonError = error as { response: { data: { extras: any } } }; diff --git a/src/services/stellar/utils.tsx b/src/services/stellar/utils.tsx index e7f19c67..a802d438 100644 --- a/src/services/stellar/utils.tsx +++ b/src/services/stellar/utils.tsx @@ -1,4 +1,4 @@ -import { Horizon, Keypair } from 'stellar-sdk'; +import { Horizon } from 'stellar-sdk'; import { HORIZON_URL } from '../../constants/constants'; import Big from 'big.js'; diff --git a/src/services/storage/remote.ts b/src/services/storage/remote.ts new file mode 100644 index 00000000..39305b8a --- /dev/null +++ b/src/services/storage/remote.ts @@ -0,0 +1,30 @@ +import { SIGNING_SERVICE_URL } from '../../constants/constants'; + +// These are the headers for the Google Spreadsheet +type Data = { + timestamp: string; + polygonAddress: string; + stellarEphemeralPublicKey: string; + pendulumEphemeralPublicKey: string; + nablaApprovalTx: string; + nablaSwapTx: string; + spacewalkRedeemTx: string; + stellarOfframpTx: string; + stellarCleanupTx: string; +}; + +export async function storeDataInBackend(data: Data) { + const response = await fetch(`${SIGNING_SERVICE_URL}/v1/storage/create`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error(`Error while sending data to storage endpoint`); + } + + console.log('Data stored successfully'); +} diff --git a/src/wagmiConfig.ts b/src/wagmiConfig.ts index ac187474..da3d8298 100644 --- a/src/wagmiConfig.ts +++ b/src/wagmiConfig.ts @@ -1,4 +1,4 @@ -import { connectorsForWallets, getDefaultConfig } from '@rainbow-me/rainbowkit'; +import { connectorsForWallets } from '@rainbow-me/rainbowkit'; import { injectedWallet, safeWallet, walletConnectWallet } from '@rainbow-me/rainbowkit/wallets'; import { polygon } from 'wagmi/chains'; import { createConfig, http } from 'wagmi'; diff --git a/vite.config.ts b/vite.config.ts index 6639f7fd..f4604b85 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ globals: true, setupFiles: ['./src/setupTests.ts'], environment: 'happy-dom', + testTimeout: 15000, }, optimizeDeps: { exclude: [], diff --git a/yarn.lock b/yarn.lock index aaf43857..a808be0d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3016,9 +3016,9 @@ __metadata: languageName: node linkType: hard -"@pendulum-chain/api-solang@npm:^0.4.0": - version: 0.4.0 - resolution: "@pendulum-chain/api-solang@npm:0.4.0" +"@pendulum-chain/api-solang@npm:^0.6.0": + version: 0.6.0 + resolution: "@pendulum-chain/api-solang@npm:0.6.0" peerDependencies: "@polkadot/api": ^10.0 "@polkadot/api-contract": ^10.12.1 @@ -3027,7 +3027,7 @@ __metadata: "@polkadot/types-codec": ^10.0 "@polkadot/util": "*" "@polkadot/util-crypto": "*" - checksum: d49eafb8ea545f89613f5dd669e47e7bf7a29db605a476a05e49e8ea44dc88ac99550e16a96f2b238644914f72d5b73bbb48a70dc81055fb01ec06ea71055b81 + checksum: 1c52568d798bfd250348be223999ccf06dadb1e6f7fc6911370da82440b142451a465c818de070de1076c7962c2c3ffd004c3eb703b80ff53f1641515923a2ef languageName: node linkType: hard @@ -13256,7 +13256,7 @@ __metadata: "@mui/icons-material": "npm:^5.14.19" "@mui/material": "npm:^5.14.20" "@pendulum-chain/api": "npm:^0.3.1" - "@pendulum-chain/api-solang": "npm:^0.4.0" + "@pendulum-chain/api-solang": "npm:^0.6.0" "@pendulum-chain/types": "npm:^0.2.3" "@polkadot/api": "npm:^9.9.1" "@polkadot/api-base": "npm:^9.9.1"