diff --git a/.github/actions/ci-setup/action.yml b/.github/actions/ci-setup/action.yml new file mode 100644 index 0000000..a9d59ad --- /dev/null +++ b/.github/actions/ci-setup/action.yml @@ -0,0 +1,12 @@ +name: 'CI setup' +description: NPM install deps +runs: + using: composite + steps: + - uses: actions/setup-node@v4 + with: + node-version: '18.20.2' + + - name: Install dependencies + shell: bash + run: npm install diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..6cceb66 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,28 @@ +name: Test Case +on: + pull_request: + branches: + - main +jobs: + + run-tests: + runs-on: ubuntu-latest + + env: + ACCOUNT_ADDRESS: ${{ secrets.ACCOUNT_ADDRESS }} + ACCOUNT_PRIVATEKEY: ${{ secrets.ACCOUNT_PRIVATEKEY }} + OPEN_PLATFORM_PRIVATE_KEY: ${{ secrets.OPEN_PLATFORM_PRIVATE_KEY }} + PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} + + steps: + - uses: actions/checkout@v3 + with: + persist-credentials: false + + - uses: ./.github/actions/ci-setup + +# - name: Build +# run: npm run build + + - name: Run Test + run: npm run test diff --git a/jest.config.js b/jest.config.js index e9d6f60..05375bc 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,12 +1,26 @@ +const { pathsToModuleNameMapper } = require('ts-jest'); + +// Assuming you have some TypeScript path aliases defined in your tsconfig.json +const { compilerOptions } = require('./tsconfig'); +/** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { - roots: ['/src'], - testMatch: [ - "**/__tests__/**/*.+(ts|tsx|js)", - "**/?(*.)+(spec|test).+(ts|tsx|js)" - ], + preset: 'ts-jest', + testEnvironment: 'node', + modulePathIgnorePatterns: ['/src/config.spec.ts'], + moduleNameMapper: { + ...pathsToModuleNameMapper({ '@/*': ['./src/*'] }, { prefix: '/' }), + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + // transformIgnorePatterns: ['node_modules/(?!(@bnb-chain/greenfield-cosmos-types)/)'], + extensionsToTreatAsEsm: ['.ts'], transform: { - "^.+\\.(ts|tsx)$": "ts-jest" + '^.+\\.ts?$': [ + 'ts-jest', + { + // tsconfig: './config/tsconfig-cjs.json', + useESM: true, + }, + ], }, - moduleDirectories: ['node_modules', 'src'], - testTimeout: 30000, -} + setupFilesAfterEnv: ['/tests/env.ts', '/tests/utils.ts'] +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b004a94..c66925e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "1.0.5", "license": "GPL-3.0-or-later", "dependencies": { - "ethers": "^6.3.0" + "dotenv": "^16.4.5", + "ethers": "^6.13.2" }, "devDependencies": { "@types/jest": "^29.2.4", "@types/node": "^18.11.10", + "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "esbuild": "^0.15.18", @@ -1419,6 +1421,15 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "node_modules/@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -2267,6 +2278,17 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -6640,6 +6662,15 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, + "@types/ws": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", + "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -7218,6 +7249,11 @@ "esutils": "^2.0.2" } }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" + }, "ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", diff --git a/package.json b/package.json index a9ed366..1a2ba43 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "devDependencies": { "@types/jest": "^29.2.4", "@types/node": "^18.11.10", + "@types/ws": "^8.5.12", "@typescript-eslint/eslint-plugin": "^5.45.0", "@typescript-eslint/parser": "^5.45.0", "esbuild": "^0.15.18", @@ -35,6 +36,7 @@ "typescript": "^4.9.3" }, "dependencies": { - "ethers": "^6.3.0" + "dotenv": "^16.4.5", + "ethers": "^6.13.2" } } diff --git a/tests/env.ts b/tests/env.ts new file mode 100644 index 0000000..dd45768 --- /dev/null +++ b/tests/env.ts @@ -0,0 +1,18 @@ +import dotenv from 'dotenv' + +dotenv.config({ + path: process.cwd() + '/tests/.env', +}) + +// testnet env +export const OPEN_PLATFORM_PRIVATE_KEY = process.env.OPEN_PLATFORM_PRIVATE_KEY +export const SPONSOR_URL = `https://open-platform.nodereal.io/${OPEN_PLATFORM_PRIVATE_KEY}/megafuel-testnet` +export const CHAIN_ID = '97' +export const CHAIN_URL = `https://bsc-testnet.nodereal.io/v1/${OPEN_PLATFORM_PRIVATE_KEY}` +export const PAYMASTER_URL = 'https://bsc-megafuel-testnet.nodereal.io/97' +export const PRIVATE_KEY = process.env.PRIVATE_KEY as string +export const POLICY_UUID = '72191372-5550-4cf6-956e-b70d1e4786cf' +export const ACCOUNT_ADDRESS = '0xF9A8db17431DD8563747D6FC770297E438Aa12eB' +export const CONTRACT_METHOD = '0xa9059cbb' +export const TOKEN_CONTRACT_ADDRESS = '0xeD24FC36d5Ee211Ea25A80239Fb8C4Cfd80f12Ee' +export const RECIPIENT_ADDRESS = '0xDE08B1Fd79b7016F8DD3Df11f7fa0FbfdF07c941' diff --git a/tests/paymaster.spec.ts b/tests/paymaster.spec.ts new file mode 100644 index 0000000..7ae9daa --- /dev/null +++ b/tests/paymaster.spec.ts @@ -0,0 +1,100 @@ +import {describe, expect, test} from '@jest/globals' +import { + paymasterProvider, + wallet, + tokenAbi, + transformIsSponsorableResponse, + transformToGaslessTransaction, + delay, transformSponsorTxResponse, +} from './utils' +import {TOKEN_CONTRACT_ADDRESS, CHAIN_ID, RECIPIENT_ADDRESS} from './env' +import {ethers} from 'ethers' + + +let TX_HASH = '' + +/** + * test paymaster apis + */ + +describe('paymasterQuery', () => { + + describe('chainID', () => { + test('it works', async () => { + const res = await paymasterProvider.chainID() + expect(res).toEqual('0x61') + }) + }) + + describe('isSponsorable', () => { + test('it works', async () => { + // Create contract instance + const tokenContract = new ethers.Contract(TOKEN_CONTRACT_ADDRESS, tokenAbi, wallet) + + // Transaction details + const tokenAmount = ethers.parseUnits('1.0', 18) // Amount of tokens to send (adjust decimals as needed) + // Get the current nonce for the sender's address + const nonce = await paymasterProvider.getTransactionCount(wallet.address, 'pending') + + + // Create the transaction object + const transaction = await tokenContract.transfer.populateTransaction(RECIPIENT_ADDRESS.toLowerCase(), tokenAmount) + + // Add nonce and gas settings + transaction.from = wallet.address + console.log('wallet.address:', transaction.from) + transaction.nonce = nonce + transaction.gasLimit = BigInt(100000) // Adjust gas limit as needed for token transfers + transaction.chainId = BigInt(CHAIN_ID) + transaction.gasPrice = BigInt(0) // Set gas price to 0 + + const safeTransaction = { + ...transaction, + gasLimit: transaction.gasLimit.toString(), + chainId: transaction.chainId.toString(), + gasPrice: transaction.gasPrice.toString(), + } + console.log(safeTransaction) + const resRaw = await paymasterProvider.isSponsorable(safeTransaction) + const res = transformIsSponsorableResponse(resRaw) + console.log(res) + expect(res.Sponsorable).toEqual(true) + + const signedTx = await wallet.signTransaction(safeTransaction) + try { + const tx = await paymasterProvider.sendRawTransaction(signedTx) + TX_HASH = tx + console.log('Transaction sent:', tx) + console.log('TX_HASH:', TX_HASH) + } catch (error) { + console.error('Error sending transaction:', error) + } + }, 100000) + }) + + describe('getGaslessTransactionByHash', () => { + test('it works', async () => { + console.log('Waiting for the transaction to be confirmed and queryable on the blockchain.') + await delay(8000) + console.log('getGaslessTransactionByHash TX_HASH:', TX_HASH) + const resRaw = await paymasterProvider.getGaslessTransactionByHash(TX_HASH) + const res = transformToGaslessTransaction(resRaw) + console.log(res) + expect(res.ToAddress).toEqual(TOKEN_CONTRACT_ADDRESS.toLowerCase()) + + console.log('getSponsorTxByBundleUuid res.BundleUUID:', res.BundleUUID) + const txRaw = await paymasterProvider.getSponsorTxByBundleUuid(res.BundleUUID) + const tx = transformSponsorTxResponse(txRaw) + expect(resRaw).not.toBeNull() + console.log(tx) + + const bundle = await paymasterProvider.getBundleByUuid(res.BundleUUID) + expect(bundle).not.toBeNull() + console.log(bundle) + + const sponsorTx = await paymasterProvider.getSponsorTxByTxHash(tx.TxHash) + console.log('sponsorTx: ', sponsorTx) + expect(sponsorTx).not.toBeNull() + }, 13000) + }) +}) diff --git a/tests/sponsor.spec.ts b/tests/sponsor.spec.ts new file mode 100644 index 0000000..e24543e --- /dev/null +++ b/tests/sponsor.spec.ts @@ -0,0 +1,152 @@ +import {describe, expect, test} from '@jest/globals' +import {client} from './utils' +import {WhitelistType} from '../src' +import {POLICY_UUID, ACCOUNT_ADDRESS, CONTRACT_METHOD} from './env' + + +/** + * test sponsor apis + */ + +describe('sponsorQuery', () => { + describe('addToWhitelist FromAccountWhitelist', () => { + test('it works', async () => { + const res = await client.addToWhitelist({ + PolicyUUID: POLICY_UUID, + WhitelistType: WhitelistType.FromAccountWhitelist, + Values: [ACCOUNT_ADDRESS], + }) + + expect(res).toEqual(true) + console.log(res) + }) + }) + + describe('addToWhitelist ToAccountWhitelist', () => { + test('it works', async () => { + const res = await client.addToWhitelist({ + PolicyUUID: POLICY_UUID, + WhitelistType: WhitelistType.ToAccountWhitelist, + Values: [ACCOUNT_ADDRESS], + }) + + expect(res).toEqual(true) + console.log(res) + }) + }) + describe('addToWhitelist BEP20ReceiverWhiteList', () => { + test('it works', async () => { + const res = await client.addToWhitelist({ + PolicyUUID: POLICY_UUID, + WhitelistType: WhitelistType.BEP20ReceiverWhiteList, + Values: [ACCOUNT_ADDRESS], + }) + + expect(res).toEqual(true) + console.log(res) + }) + }) + + describe('addToWhitelist ContractMethodSigWhitelist', () => { + test('it works', async () => { + const res = await client.addToWhitelist({ + PolicyUUID: POLICY_UUID, + WhitelistType: WhitelistType.ContractMethodSigWhitelist, + Values: [CONTRACT_METHOD], + }) + + expect(res).toEqual(true) + }) + }) + + describe('getWhitelist', () => { + test('it works', async () => { + const res = await client.getWhitelist({ + PolicyUUID: POLICY_UUID, + WhitelistType: WhitelistType.ContractMethodSigWhitelist, + Offset: 0, + Limit: 10, + }) + + expect(res[0]).toEqual(CONTRACT_METHOD) + }) + }) + + describe('removeFromWhitelist', () => { + test('it works', async () => { + const res = await client.removeFromWhitelist({ + PolicyUUID: POLICY_UUID, + WhitelistType: WhitelistType.FromAccountWhitelist, + Values: [ACCOUNT_ADDRESS], + }) + + expect(res).toEqual(true) + }) + }) + + describe('getWhitelist', () => { + test('it works', async () => { + const res = await client.getWhitelist({ + PolicyUUID: POLICY_UUID, + WhitelistType: WhitelistType.FromAccountWhitelist, + Offset: 0, + Limit: 10, + }) + if (res !== null && res !== undefined) { + expect(res).not.toContain(ACCOUNT_ADDRESS) + } + }) + }) + + describe('emptyWhitelist', () => { + test('it works', async () => { + const res = await client.emptyWhitelist({ + PolicyUUID: POLICY_UUID, + WhitelistType: WhitelistType.BEP20ReceiverWhiteList, + }) + + expect(res).toEqual(true) + }) + }) + + describe('getWhitelist', () => { + test('it works', async () => { + const res = await client.getWhitelist({ + PolicyUUID: POLICY_UUID, + WhitelistType: WhitelistType.BEP20ReceiverWhiteList, + Offset: 0, + Limit: 10, + }) + + expect(res).toBeNull() + }) + }) + + describe('getUserSpendData', () => { + test('it works', async () => { + const res = await client.getUserSpendData(ACCOUNT_ADDRESS, POLICY_UUID) + + expect(res).toBeNull() + }) + }) + + describe('getPolicySpendData', () => { + test('it works', async () => { + const res = await client.getPolicySpendData(POLICY_UUID) + expect(res.ChainID).not.toBeNull() + }) + }) + + describe('addToWhitelist FromAccountWhitelist', () => { + test('it works', async () => { + const res = await client.addToWhitelist({ + PolicyUUID: POLICY_UUID, + WhitelistType: WhitelistType.FromAccountWhitelist, + Values: [ACCOUNT_ADDRESS], + }) + + expect(res).toEqual(true) + console.log(res) + }) + }) +}) diff --git a/tests/utils.ts b/tests/utils.ts new file mode 100644 index 0000000..ea32055 --- /dev/null +++ b/tests/utils.ts @@ -0,0 +1,65 @@ +import {SponsorClient, PaymasterClient, IsSponsorableResponse, GaslessTransaction, SponsorTx} from '../src' +import {CHAIN_ID, SPONSOR_URL, CHAIN_URL, PAYMASTER_URL, PRIVATE_KEY, TOKEN_CONTRACT_ADDRESS} from './env' +import {ethers} from 'ethers' + +export const client = new SponsorClient(SPONSOR_URL, undefined, {staticNetwork: ethers.Network.from(Number(CHAIN_ID))}) + +// Provider for assembling the transaction (e.g., mainnet) +export const assemblyProvider = new ethers.JsonRpcProvider(CHAIN_URL) + +// Provider for sending the transaction (e.g., could be a different network or provider) +export const paymasterProvider = new PaymasterClient(PAYMASTER_URL) + +export const wallet = new ethers.Wallet(PRIVATE_KEY, assemblyProvider) +// ERC20 token ABI (only including the transfer function) +export const tokenAbi = [ + 'function transfer(address,uint256) returns (bool)', +] + +// Create contract instance +export const tokenContract = new ethers.Contract(TOKEN_CONTRACT_ADDRESS, tokenAbi, wallet) + +export function transformIsSponsorableResponse(rawResponse: any): IsSponsorableResponse { + return { + Sponsorable: rawResponse.sponsorable, + SponsorName: rawResponse.sponsorName, + SponsorIcon: rawResponse.sponsorIcon, + SponsorWebsite: rawResponse.sponsorWebsite, + } +} + +export function transformToGaslessTransaction(rawResponse: any): GaslessTransaction { + return { + TxHash: rawResponse.txHash, + BundleUUID: rawResponse.bundleUuid, + FromAddress: rawResponse.fromAddress, + ToAddress: rawResponse.toAddress, + Nonce: rawResponse.nonce, + RawData: rawResponse.rawData, + Status: rawResponse.status, + GasUsed: rawResponse.gasUsed, + GasFee: rawResponse.gasFee, + PolicyUUID: rawResponse.policyUuid, + Source: rawResponse.source, + BornBlockNumber: rawResponse.bornBlockNumber, + ChainID: rawResponse.chainId, + } +} + +export function transformSponsorTxResponse(rawResponse: any): SponsorTx { + return { + TxHash: rawResponse.txHash || '', + Address: rawResponse.address, // Ensure this is handled if it needs special type treatment like AddressLike + BundleUUID: rawResponse.bundleUuid || '', + Status: rawResponse.status, // Assuming status is directly assignable + GasPrice: rawResponse.gasPrice, // Handle if necessary, ensure it's a BigNumberish type if required + GasFee: rawResponse.gasFee, // Similarly handle BigNumberish + BornBlockNumber: BigInt(rawResponse.bornBlockNumber || 0), // Default to 0 if undefined + ChainID: rawResponse.chainId || 0, // Default to 0 if undefined + } +} + +// Function to create a delay +export function delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} diff --git a/tsconfig.json b/tsconfig.json index cb50ef9..dda1909 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,8 +8,8 @@ "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, - "noUnusedLocals": true, - "noUnusedParameters": true, + "noUnusedLocals": false, + "noUnusedParameters": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "importHelpers": true,