mirror of
https://github.com/alexgo-io/xverse-stacks-transaction-sponsor.git
synced 2026-01-12 08:43:39 +08:00
Initial commit
This commit is contained in:
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
||||
SEED="your sponsor wallet seed phrase"
|
||||
PASSWORD="sponsoredbyxverse"
|
||||
NUM_ADDRESSES=5
|
||||
MAX_FEE=50000
|
||||
108
.gitignore
vendored
Normal file
108
.gitignore
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
env
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and *not* Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# ignore ds_store files created in mac os
|
||||
.DS_Store
|
||||
46
README.md
Normal file
46
README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
## Stacks Transaction Sponsor Web Service
|
||||
|
||||
This is a web service that functions as a open sponsor for Stacks transactions. This allows users to send transactions for free, without owning any STX tokens in their wallet.
|
||||
|
||||
POST to the `/v1/sponsor` endpoint with the user signed transaction in the JSON body. The transaction must be created as a sponsored transaction. The service will sign the transaction as the sponsor and broadcast it.
|
||||
|
||||
The service will use multiple addresses from the same wallet at random to sponsor transactions. You can set the number of addresses to use in the `.env` file. Each addresses must be funded with STX. This allows the sponsor to handle more simultaneous pending transactions.
|
||||
|
||||
`/v1/info` will return the list of addresses used by the service for sponsoring transactions
|
||||
|
||||
**NOTE: Rules are currently set so that only NFT transfer transaction will be sponsored. You can modify the rules for your own use if you are forking.**
|
||||
|
||||
## Requirements
|
||||
|
||||
* [Node Js](https://nodejs.org/en/download)
|
||||
|
||||
## Setup Instructions
|
||||
|
||||
Clone the repo and install the dependencies.
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
Create an `.env` file in the project root using the included `.env.example` file as a guide.
|
||||
|
||||
Configure the .env file with your wallet seed phrase.
|
||||
|
||||
|
||||
## Running
|
||||
|
||||
To start the server, run the following
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
To run in development mode
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
To run tests
|
||||
```
|
||||
npm run test
|
||||
```
|
||||
|
||||
36
config/config.ts
Normal file
36
config/config.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
let result;
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
result = dotenv.config({ path: "/usr/src/app/env-config/env" });
|
||||
} else {
|
||||
result = dotenv.config();
|
||||
}
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
//will check and return envVar if required env variable is present in .env file
|
||||
function isEnvVarValid(envVar: string) {
|
||||
if (envVar === undefined || null) {
|
||||
throw new Error(
|
||||
"Incorrect env variable format! Compare with .env.example."
|
||||
);
|
||||
}
|
||||
return envVar;
|
||||
}
|
||||
|
||||
export default {
|
||||
seed: isEnvVarValid(
|
||||
process.env.SEED as string
|
||||
),
|
||||
password: isEnvVarValid(
|
||||
process.env.PASSWORD as string
|
||||
),
|
||||
numAddresses: isEnvVarValid(
|
||||
process.env.NUM_ADDRESSES as string
|
||||
),
|
||||
maxFee: isEnvVarValid(
|
||||
process.env.MAX_FEE as string
|
||||
),
|
||||
};
|
||||
3836
package-lock.json
generated
Normal file
3836
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
35
package.json
Normal file
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "stacks-transaction-sponsor",
|
||||
"version": "0.1.0",
|
||||
"description": "Stacks Transaction Sponsor",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "tsc --project ./",
|
||||
"dev": "nodemon src/index.ts",
|
||||
"start": "node dist/src/index.js",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch --coverage=false"
|
||||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@stacks/common": "^6.0.0",
|
||||
"@stacks/network": "^6.0.0",
|
||||
"@stacks/transactions": "^6.0.0",
|
||||
"@stacks/wallet-sdk": "^6.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"axios": "^0.21.4",
|
||||
"body-parser": "^1.19.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.17.1",
|
||||
"node-cache": "^5.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.12.6",
|
||||
"nock": "^13.2.0",
|
||||
"nodemon": "^2.0.15",
|
||||
"ts-node": "^10.8.1",
|
||||
"typescript": "^4.2.3"
|
||||
}
|
||||
}
|
||||
101
src/api/v1/controller.ts
Normal file
101
src/api/v1/controller.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import {
|
||||
deserializeTransaction,
|
||||
sponsorTransaction,
|
||||
broadcastTransaction,
|
||||
SponsoredAuthorization
|
||||
} from "@stacks/transactions";
|
||||
import { bytesToHex } from "@stacks/common";
|
||||
import { StacksMainnet } from '@stacks/network';
|
||||
import envVariables from '../../../config/config';
|
||||
import {
|
||||
SponsorAccountsKey,
|
||||
} from '../../constants'
|
||||
import {
|
||||
lockRandomSponsorAccount,
|
||||
getAccountNonce,
|
||||
incrementAccountNonce,
|
||||
unlockSponsorAccount,
|
||||
check
|
||||
} from '../../nonce';
|
||||
import {
|
||||
getAccountAddress
|
||||
} from '../../utils';
|
||||
import {
|
||||
validateTransaction
|
||||
} from '../../validation';
|
||||
|
||||
let cache = require('../../cache');
|
||||
|
||||
export class Controller {
|
||||
info = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const accounts = cache.instance().get(SponsorAccountsKey);
|
||||
const addresses = [];
|
||||
accounts.forEach(account => {
|
||||
const address = getAccountAddress(account);
|
||||
addresses.push(address);
|
||||
});
|
||||
|
||||
return res.json({
|
||||
active: true,
|
||||
sponsor_addresses: addresses
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
|
||||
sponsor = async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const rawTx = req.body.tx;
|
||||
const tx = deserializeTransaction(rawTx);
|
||||
|
||||
// check against rules to see if transaction is allowed for sponsorship
|
||||
const validTx = await validateTransaction(tx);
|
||||
|
||||
if (!validTx) {
|
||||
throw new Error('Transaction not valid for sponsorship');
|
||||
}
|
||||
|
||||
// attempt to lock a random sponsor account from wallet
|
||||
const account = await lockRandomSponsorAccount();
|
||||
|
||||
// get the sponsor address nonce
|
||||
const network = new StacksMainnet();
|
||||
const nonce = getAccountNonce(account);
|
||||
|
||||
// sign transaction as sponsor
|
||||
const signedTx = await sponsorTransaction({
|
||||
transaction: tx,
|
||||
sponsorPrivateKey: account.stxPrivateKey,
|
||||
network,
|
||||
sponsorNonce: nonce
|
||||
});
|
||||
|
||||
// make sure fee doesn't exceed maximum
|
||||
const auth = signedTx.auth as SponsoredAuthorization;
|
||||
const fee = auth.sponsorSpendingCondition.fee;
|
||||
const maxFee = BigInt(envVariables.maxFee);
|
||||
if (fee > maxFee) {
|
||||
throw new Error(`Transaction fee (${fee}) exceeds max sponsor fee (${maxFee})`);
|
||||
}
|
||||
|
||||
// broadcast transaction
|
||||
const result = await broadcastTransaction(signedTx, network);
|
||||
|
||||
// increment nonce, release sponsor account and handle errors
|
||||
if ('error' in result) {
|
||||
unlockSponsorAccount(account);
|
||||
throw new Error(`Broadcast failed: ${result.error}`);
|
||||
} else {
|
||||
incrementAccountNonce(account);
|
||||
unlockSponsorAccount(account);
|
||||
}
|
||||
|
||||
return res.json({txid: result.txid, rawTx: bytesToHex(signedTx.serialize())});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
10
src/api/v1/router.ts
Normal file
10
src/api/v1/router.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Controller } from "./controller";
|
||||
|
||||
const express = require("express");
|
||||
const router = express.Router();
|
||||
const controller = new Controller();
|
||||
|
||||
router.get("/info", controller.info);
|
||||
router.post("/sponsor", controller.sponsor);
|
||||
|
||||
export = router;
|
||||
12
src/cache.ts
Normal file
12
src/cache.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
let nodeCache = require('node-cache')
|
||||
let cache = null
|
||||
|
||||
exports.start = function (done) {
|
||||
if (cache) return done()
|
||||
|
||||
cache = new nodeCache()
|
||||
}
|
||||
|
||||
exports.instance = function () {
|
||||
return cache
|
||||
}
|
||||
4
src/constants.ts
Normal file
4
src/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const SponsorAccountsKey = "SponsorAccounts";
|
||||
export const AddressNoncePrefix = "Nonce-";
|
||||
export const AddressLockPrefix = "Lock-";
|
||||
export const MaxLockAttempts = 10;
|
||||
39
src/index.ts
Normal file
39
src/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { json } from 'body-parser';
|
||||
import { errorHandler, wrongRouteHandler } from "./response";
|
||||
import { initializeSponsorWallet } from './initialization';
|
||||
|
||||
let cache = require('./cache')
|
||||
|
||||
const express = require("express");
|
||||
var cors = require('cors')
|
||||
const app = express();
|
||||
|
||||
const v1 = require("./api/v1/router");
|
||||
|
||||
let port = 8080;
|
||||
if (app.get("env") === "development") {
|
||||
port = 3100;
|
||||
}
|
||||
|
||||
cache.start(function (err) {
|
||||
if (err) console.error(err)
|
||||
})
|
||||
|
||||
// initialize the sponsor wallets to the correct nonce
|
||||
initializeSponsorWallet();
|
||||
|
||||
app.use(json());
|
||||
app.use(cors());
|
||||
app.use("/v1", v1);
|
||||
app.use(errorHandler);
|
||||
app.use(wrongRouteHandler);
|
||||
|
||||
const server = app.listen(port, () => {
|
||||
console.log(
|
||||
`Stacks Transaction Sponsor Web Service started and listening at http://localhost:${port} in ${app.get(
|
||||
"env"
|
||||
)} mode`
|
||||
);
|
||||
});
|
||||
|
||||
export default server;
|
||||
91
src/initialization.ts
Normal file
91
src/initialization.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { StacksMainnet } from "@stacks/network";
|
||||
import {
|
||||
getAddressFromPrivateKey,
|
||||
TransactionVersion
|
||||
} from "@stacks/transactions";
|
||||
import envVariables from "../config/config";
|
||||
import axios from "axios";
|
||||
import {
|
||||
generateWallet,
|
||||
generateNewAccount
|
||||
} from "@stacks/wallet-sdk";
|
||||
import {
|
||||
SponsorAccountsKey,
|
||||
AddressNoncePrefix
|
||||
} from './constants'
|
||||
|
||||
let cache = require('./cache')
|
||||
|
||||
export interface StxAddressDataResponse {
|
||||
balance: string;
|
||||
locked: string;
|
||||
unlock_height: string;
|
||||
nonce: number;
|
||||
}
|
||||
|
||||
export async function initializeSponsorWallet() {
|
||||
const seed = envVariables.seed;
|
||||
const password = envVariables.password;
|
||||
const numAddresses = Number(envVariables.numAddresses);
|
||||
|
||||
// generate wallet from seed
|
||||
const wallet = await generateWallet({
|
||||
secretKey: seed,
|
||||
password: password,
|
||||
});
|
||||
|
||||
// derive specified number of accounts/addresses
|
||||
for (let i = 0; i < numAddresses - 1; i++) {
|
||||
const newAccounts = await generateNewAccount(wallet);
|
||||
wallet.accounts = newAccounts.accounts;
|
||||
}
|
||||
|
||||
// cache sponsor accounts
|
||||
const result = cache.instance().set(SponsorAccountsKey, wallet.accounts);
|
||||
|
||||
// get the correct next nonce for each addresses
|
||||
wallet.accounts.forEach(account => {
|
||||
const address = getAddressFromPrivateKey(account.stxPrivateKey, TransactionVersion.Mainnet);
|
||||
setupAccountNonce(address);
|
||||
});
|
||||
}
|
||||
|
||||
export async function setupAccountNonce(address: string) {
|
||||
const network = new StacksMainnet();
|
||||
|
||||
const apiUrl = `${network.coreApiUrl}/v2/accounts/${address}?proof=0`;
|
||||
|
||||
// get current nonce
|
||||
const balanceInfo = await axios.get<StxAddressDataResponse>(apiUrl, {
|
||||
timeout: 30000,
|
||||
});
|
||||
const nonce = balanceInfo.data.nonce;
|
||||
|
||||
// get pending transactions, increment nonce for each
|
||||
const pendingTransactionCount = await getMempoolTransactions(address);
|
||||
|
||||
// calculate correct next nonce
|
||||
const nextNonce = nonce + pendingTransactionCount;
|
||||
|
||||
// cache correct next nonce
|
||||
const result = cache.instance().set(AddressNoncePrefix+address, nextNonce);
|
||||
}
|
||||
|
||||
export async function getMempoolTransactions(
|
||||
stxAddress: string
|
||||
): Promise<number> {
|
||||
const network = new StacksMainnet();
|
||||
let apiUrl = `${network.coreApiUrl}/extended/v1/tx/mempool?address=${stxAddress}`;
|
||||
|
||||
return axios
|
||||
.get(apiUrl, {
|
||||
timeout: 30000,
|
||||
params: {
|
||||
limit: 0,
|
||||
offset: 30,
|
||||
},
|
||||
})
|
||||
.then((response) => {
|
||||
return response.data.total;
|
||||
});
|
||||
}
|
||||
84
src/nonce.ts
Normal file
84
src/nonce.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
SponsorAccountsKey,
|
||||
AddressNoncePrefix,
|
||||
AddressLockPrefix,
|
||||
MaxLockAttempts
|
||||
} from './constants'
|
||||
|
||||
import { Account } from '@stacks/wallet-sdk';
|
||||
|
||||
import envVariables from "../config/config";
|
||||
|
||||
import {
|
||||
sleep,
|
||||
getRandomInt,
|
||||
getAccountAddress
|
||||
} from './utils';
|
||||
|
||||
let cache = require('./cache');
|
||||
|
||||
export function check(address: string): boolean {
|
||||
return cache.instance().get(AddressLockPrefix+address);
|
||||
}
|
||||
|
||||
// lock address nonce
|
||||
export function lock(address: string): boolean {
|
||||
const lock = check(address);
|
||||
if (lock === true) {
|
||||
return false;
|
||||
} else {
|
||||
cache.instance().set(AddressLockPrefix+address, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// unlock address nonce
|
||||
export function unlock(address: string) {
|
||||
cache.instance().set(AddressLockPrefix+address, false);
|
||||
}
|
||||
|
||||
export function getRandomSponsorAccount(): Account {
|
||||
const numAddresses = Number(envVariables.numAddresses);
|
||||
const randomIndex = getRandomInt(0, numAddresses - 1);
|
||||
const accounts = cache.instance().get(SponsorAccountsKey);
|
||||
return accounts[randomIndex];
|
||||
}
|
||||
|
||||
// attempt to lock one of the available addresses
|
||||
// retry after 1000ms if locked by another request
|
||||
export async function lockRandomSponsorAccount(): Promise<Account> {
|
||||
var locked = false;
|
||||
var attempts = 0;
|
||||
|
||||
while(!locked) {
|
||||
const account = getRandomSponsorAccount();
|
||||
const address = getAccountAddress(account);
|
||||
locked = lock(address);
|
||||
if (locked) {
|
||||
return account;
|
||||
} else {
|
||||
attempts++;
|
||||
await sleep(1000);
|
||||
if (attempts >= MaxLockAttempts) {
|
||||
throw new Error('Reached max attempts while locking sponsor address');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function unlockSponsorAccount(account: Account) {
|
||||
const address = getAccountAddress(account);
|
||||
unlock(address);
|
||||
}
|
||||
|
||||
export function getAccountNonce(account: Account) {
|
||||
const address = getAccountAddress(account);
|
||||
const nonce = cache.instance().get(AddressNoncePrefix+address);
|
||||
return nonce;
|
||||
}
|
||||
|
||||
export function incrementAccountNonce(account: Account) {
|
||||
const address = getAccountAddress(account);
|
||||
const currentNonce = getAccountNonce(account);
|
||||
return cache.instance().set(AddressNoncePrefix+address, currentNonce + 1);
|
||||
}
|
||||
15
src/response.ts
Normal file
15
src/response.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function errorHandler(err, req, res, next) {
|
||||
err.statusCode = err.statusCode || 400;
|
||||
res.status(err.statusCode).json({
|
||||
status: err.statusCode,
|
||||
message: err.message
|
||||
})
|
||||
}
|
||||
|
||||
export function wrongRouteHandler(req, res, next) {
|
||||
const statusCode = 404;
|
||||
res.status(statusCode).json({
|
||||
status: statusCode,
|
||||
message: "Route not found. Request failed with status code 404 "
|
||||
})
|
||||
}
|
||||
24
src/utils.ts
Normal file
24
src/utils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Account } from '@stacks/wallet-sdk';
|
||||
import {
|
||||
getAddressFromPrivateKey,
|
||||
TransactionVersion
|
||||
} from "@stacks/transactions";
|
||||
|
||||
export function getRandomInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
export function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export function getAccountAddress(
|
||||
account: Account,
|
||||
version: TransactionVersion = TransactionVersion.Mainnet
|
||||
): string {
|
||||
return getAddressFromPrivateKey(account.stxPrivateKey, version);
|
||||
}
|
||||
43
src/validation.ts
Normal file
43
src/validation.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
PayloadType,
|
||||
ContractCallPayload,
|
||||
getAbi,
|
||||
addressToString,
|
||||
StacksTransaction
|
||||
} from "@stacks/transactions";
|
||||
|
||||
// rules for transaction sponsorship eligibility
|
||||
// customize this if you are forking this repo
|
||||
export async function validateTransaction(transaction: StacksTransaction) {
|
||||
if (transaction.payload.payloadType !== PayloadType.ContractCall) {
|
||||
throw new Error('Transaction is not a contract call');
|
||||
}
|
||||
|
||||
const payload = transaction.payload as ContractCallPayload;
|
||||
|
||||
// check and limit to SIP-09 transfers calls
|
||||
if (payload.functionName.content.toString() !== 'transfer') {
|
||||
throw new Error('Transaction is not a NFT transfer contract call');
|
||||
}
|
||||
|
||||
const contractAddress = addressToString(payload.contractAddress);
|
||||
const contractName = payload.contractName.content.toString();
|
||||
const functionName = payload.functionName.content.toString();
|
||||
const abi = await getAbi(contractAddress, contractName, "mainnet");
|
||||
const func = abi.functions.find((func) => {
|
||||
return func.name === functionName;
|
||||
})
|
||||
|
||||
if (func.args.length !== 3) {
|
||||
throw new Error('Transaction is not a NFT transfer contract call');
|
||||
}
|
||||
|
||||
// check against sip-09 interface for transfer
|
||||
if (func.args[0].name !== 'id' || func.args[0].type !== 'uint128'
|
||||
|| func.args[1].name !== 'sender' || func.args[1].type !== 'principal'
|
||||
|| func.args[2].name !== 'recipient' || func.args[2].type !== 'principal') {
|
||||
throw new Error('Transaction is not a NFT transfer contract call');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"lib": [
|
||||
"dom", "ES2020.Promise",
|
||||
],
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"noImplicitAny": false,
|
||||
"strictNullChecks": false,
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user