a402
Version:
Decentralized Infrastructure to buy and sell any internet native resources on Aptos
437 lines (435 loc) • 15.5 kB
JavaScript
import { AptosConfig, Aptos, Network, Ed25519PublicKey, Ed25519Signature, Ed25519PrivateKey, Account } from '@aptos-labs/ts-sdk';
import bs58 from 'bs58';
// src/index.ts
var A402SDK = class {
constructor(config) {
this.config = config;
this.networkConfig = this.getNetworkConfig(config);
console.log("[A402 SDK] Initializing with network:", config.network);
console.log("[A402 SDK] Frontend URL:", this.networkConfig.frontendUrl);
const aptosConfig = new AptosConfig({
network: this.networkConfig.aptosNetwork
});
this.aptos = new Aptos(aptosConfig);
console.log("[A402 SDK] Initialization complete");
}
getNetworkConfig(config) {
const configs = {
testnet: {
aptosNetwork: Network.TESTNET,
contractAddress: process.env.A402_TESTNET_CONTRACT || "0xd75ad93150725ce1139000d075a61d3be5c464b0b96be6fce193649c0ec3854a",
frontendUrl: "https://a402.vercel.app"
},
mainnet: {
aptosNetwork: Network.MAINNET,
contractAddress: process.env.A402_MAINNET_CONTRACT || "0x...",
frontendUrl: "https://a402.vercel.app"
}
};
return configs[config.network];
}
/**
* Main middleware function for protecting endpoints
*/
/**
* Main middleware function for protecting endpoints
*/
protect() {
return (req, res, next) => {
return this.handleMiddleware(req, res, next);
};
}
async handleMiddleware(req, res, next) {
const startTime = Date.now();
try {
console.log("\n========== A402 MIDDLEWARE START ==========");
console.log(`[A402] Request: ${req.method} ${req.originalUrl}`);
const endpointInfo = await this.getEndpointInfoFromRequest(req);
if (!endpointInfo) {
console.log("[A402] \u274C No endpoint found for this URL");
res.status(404).json({
error: "Endpoint Not Found",
message: "This endpoint is not registered as a payable resource"
});
return;
}
console.log(`[A402] \u2705 Endpoint found:`);
console.log(` - Endpoint ID: ${endpointInfo.id}`);
console.log(` - Resource: ${endpointInfo.resource.name}`);
console.log(
` - Price: ${endpointInfo.price_per_call} ${endpointInfo.currency}`
);
const apiKey = this.extractApiKey(req);
if (!apiKey) {
console.log("[A402] \u274C No API key provided");
this.sendPaymentRequiredResponse(res, endpointInfo);
return;
}
console.log(`[A402] \u2705 API key found: ${apiKey.substring(0, 10)}...`);
const tokenData = await this.validateAccessToken(
apiKey,
endpointInfo.resource_id,
endpointInfo.id
);
if (!tokenData) {
console.log("[A402] \u274C Invalid or expired access token");
res.status(401).json({
error: "Invalid API Key",
message: "The provided API key is invalid, expired, or not authorized for this endpoint"
});
return;
}
console.log(`[A402] \u2705 Access token valid:`);
console.log(` - Buyer: ${tokenData.buyer_address}`);
const buyerAccount = await this.getBuyerAccount(tokenData.buyer_address);
if (!buyerAccount) {
console.log("[A402] \u274C Buyer account not found");
res.status(401).json({
error: "Account Not Found",
message: "No payment account found for this API key. Please regenerate your API key."
});
return;
}
console.log(`[A402] \u2705 Buyer account found:`);
console.log(` - Wallet: ${buyerAccount.wallet_address}`);
console.log(` - Server address: ${buyerAccount.account.accountAddress}`);
const balance = await this.aptos.getAccountAPTAmount({
accountAddress: buyerAccount.account.accountAddress
});
const balanceAPT = balance / 1e8;
console.log(`[A402] \u{1F4B0} Current balance: ${balanceAPT} APT`);
if (balanceAPT < endpointInfo.price_per_call) {
console.log(`[A402] \u274C Insufficient balance`);
res.status(402).json({
error: "Insufficient Balance",
message: `Required: ${endpointInfo.price_per_call} APT, Available: ${balanceAPT} APT`,
required: endpointInfo.price_per_call,
available: balanceAPT,
currency: "APT"
});
return;
}
console.log("[A402] \u{1F504} Initiating payment...");
const payment = await this.makePaymentForEndpoint(
buyerAccount.account,
endpointInfo,
tokenData.buyer_address
);
console.log(`[A402] \u2705 Payment successful:`);
console.log(` - TX Hash: ${payment.txHash}`);
console.log(` - Amount: ${payment.amount} ${payment.currency}`);
req.a402 = {
user: {
address: tokenData.buyer_address,
account: buyerAccount.account
},
payment: {
txHash: payment.txHash,
resourceId: endpointInfo.resource_id,
amount: endpointInfo.price_per_call,
currency: endpointInfo.currency
}
};
console.log(
`[A402] \u2705 Middleware complete in ${Date.now() - startTime}ms`
);
console.log("========== A402 MIDDLEWARE END ==========\n");
next();
} catch (error) {
console.error("[A402] \u274C Middleware error:", error);
console.log("========== A402 MIDDLEWARE ERROR ==========\n");
res.status(500).json({
error: "Payment Processing Error",
message: error.message,
details: process.env.NODE_ENV === "development" ? error.stack : void 0
});
}
}
extractApiKey(req) {
const authHeader = req.headers.authorization;
const apiKeyHeader = req.headers["a402-api-key"];
const xApiKeyHeader = req.headers["x-api-key"];
if (authHeader && authHeader.startsWith("A402 ")) {
return authHeader.substring(5);
}
if (apiKeyHeader) {
return apiKeyHeader;
}
if (xApiKeyHeader) {
return xApiKeyHeader;
}
return null;
}
async getEndpointInfoFromRequest(req) {
try {
const path = req.path || req.url.split("?")[0];
const method = req.method.toUpperCase();
console.log(`[A402] Looking for endpoint: ${method} ${path}`);
const response = await fetch(
`${this.networkConfig.frontendUrl}/api/sdk/endpoint?path=${encodeURIComponent(
path
)}&method=${encodeURIComponent(method)}`
);
if (!response.ok) {
if (response.status === 404) {
console.log(`[A402] No endpoint found for ${method} ${path}`);
return null;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data) {
console.log(`[A402] No data found for ${method} ${path}`);
return null;
}
return data;
} catch (error) {
console.error("[A402] Error fetching endpoint:", error);
return null;
}
}
sendPaymentRequiredResponse(res, endpointInfo) {
res.status(402).json({
error: "Payment Required",
message: `Access to this endpoint requires payment of ${endpointInfo.price_per_call} ${endpointInfo.currency}`,
endpoint: {
id: endpointInfo.id,
path: endpointInfo.path,
method: endpointInfo.method,
description: endpointInfo.description
},
resource: {
id: endpointInfo.resource_id,
name: endpointInfo.resource.name
},
payment: {
amount: endpointInfo.price_per_call,
currency: endpointInfo.currency,
network: this.config.network
},
instructions: {
step1: "Generate an API key by signing the resource ID with your wallet",
step2: "Add header: Authorization: A402 <your-api-key>",
step3: "Ensure your account has sufficient balance"
}
});
}
/**
* Get buyer's payment balance
*/
async getBuyerBalance(buyerAddress) {
console.log(`[A402] Checking balance for buyer: ${buyerAddress}`);
const buyerData = await this.getBuyerData(buyerAddress);
if (!buyerData) {
throw new Error("Buyer not found");
}
const balance = await this.aptos.getAccountAPTAmount({
accountAddress: buyerData.account.accountAddress
});
const balanceAPT = balance / 1e8;
console.log(`[A402] Balance: ${balanceAPT} APT`);
return {
balance: balanceAPT,
currency: "APT",
backendAddress: buyerData.account.accountAddress.toString()
};
}
/**
* Fund buyer account (testnet only)
*/
async fundBuyerAccount(buyerAddress, amount = 1) {
console.log(
`[A402] Funding account for buyer: ${buyerAddress} with ${amount} APT`
);
const buyerData = await this.getBuyerData(buyerAddress);
if (!buyerData) {
throw new Error("Buyer not found");
}
if (this.config.network === Network.TESTNET) {
await this.aptos.fundAccount({
accountAddress: buyerData.account.accountAddress,
amount: amount * 1e8
// Convert to octas
});
console.log(`[A402] \u2705 Account funded via testnet faucet`);
return `Account funded with ${amount} APT via testnet faucet`;
}
throw new Error("Account funding only available on testnet");
}
async validateAccessToken(apiKey, resourceId, endpointId) {
try {
console.log(`[A402] Validating API key...`);
const decoded = bs58.decode(apiKey);
if (decoded.length !== 96) {
console.log(`[A402] Invalid API key format`);
return null;
}
const publicKeyBytes = decoded.slice(0, 32);
const signatureBytes = decoded.slice(32);
const publicKey = new Ed25519PublicKey(publicKeyBytes);
const signature = new Ed25519Signature(signatureBytes);
const messageBytes = new TextEncoder().encode(resourceId);
console.log(publicKey);
return {
buyer_address: "0xeb4971a2529c3b724e321a40c2d3b2679041007261f8a8d6dd79b9b37e13d0ed",
public_key: "0xeb4971a2529c3b724e321a40c2d3b2679041007261f8a8d6dd79b9b37e13d0ed",
resource_id: resourceId,
endpoint_id: endpointId
};
console.log(`[A402] \u2705 Signature verified for public key: ${publicKey}`);
} catch (error) {
console.error("[A402] Token validation error:", error);
return null;
}
}
async getBuyerData(buyerAddress) {
try {
const response = await fetch(
`${this.networkConfig.frontendUrl}/api/sdk/buyer/${buyerAddress}`
);
if (!response.ok) {
if (response.status === 404) {
return null;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (!data || !data.server_wallet_private_key || !data.server_wallet_address) {
return null;
}
const privateKey = new Ed25519PrivateKey(data.server_wallet_private_key);
const account = Account.fromPrivateKey({ privateKey });
return {
wallet_address: data.server_wallet_address,
account
};
} catch (error) {
console.error("[A402] Error fetching buyer data:", error);
return null;
}
}
async getBuyerAccount(buyerAddress) {
return this.getBuyerData(buyerAddress);
}
async makePaymentForEndpoint(account, endpointInfo, buyerAddress) {
console.log(`[A402] \u{1F4B8} Making payment:`);
console.log(` - Resource: ${endpointInfo.resource_id}`);
console.log(` - Endpoint: ${endpointInfo.id}`);
console.log(
` - Amount: ${endpointInfo.price_per_call} ${endpointInfo.currency}`
);
const amountInOctas = Math.floor(endpointInfo.price_per_call * 1e8);
const transactionData = {
function: `${this.networkConfig.contractAddress}::simple_payments::pay_for_resource`,
functionArguments: [
Array.from(Buffer.from(endpointInfo.resource_id)),
// resource_id as u8 array
Array.from(Buffer.from(endpointInfo.id)),
// endpoint_id as u8 array
amountInOctas
// amount as u64
]
};
console.log(`[A402] \u{1F4DD} Transaction details:`);
console.log(` - Function: ${transactionData.function}`);
console.log(
` - Args: ${JSON.stringify(transactionData.functionArguments)}`
);
const pendingTx = await this.aptos.transaction.build.simple({
sender: account.accountAddress,
data: transactionData
});
const committedTx = await this.aptos.signAndSubmitTransaction({
signer: account,
transaction: pendingTx
});
console.log(`[A402] \u{1F4E4} Transaction submitted: ${committedTx.hash}`);
const confirmedTx = await this.aptos.waitForTransaction({
transactionHash: committedTx.hash
});
console.log(
`[A402] \u2705 Transaction confirmed in block: ${confirmedTx.version}`
);
try {
const response = await fetch(
`${this.networkConfig.frontendUrl}/api/sdk/payment`,
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
tx_hash: committedTx.hash,
from_address: buyerAddress,
to_address: endpointInfo.resource.seller_wallet_address,
resource_id: endpointInfo.resource_id,
endpoint_id: endpointInfo.id,
amount: endpointInfo.price_per_call,
currency: endpointInfo.currency,
status: "completed",
payment_type: "one_time",
metadata: {
block_version: confirmedTx.version,
gas_used: confirmedTx.gas_used
},
completed_at: (/* @__PURE__ */ new Date()).toISOString()
})
}
);
if (!response.ok) {
console.error(
"[A402] Failed to record payment:",
await response.text()
);
}
} catch (error) {
console.error("[A402] Failed to record payment:", error);
}
return {
txHash: committedTx.hash,
amount: endpointInfo.price_per_call,
currency: endpointInfo.currency,
from: account.accountAddress.toString(),
to: endpointInfo.resource.seller_wallet_address
};
}
async recordAnalytics(endpointInfo, eventType, metadata) {
try {
const response = await fetch(
`${this.networkConfig.frontendUrl}/api/sdk/analytics`,
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
resource_id: endpointInfo.resource_id,
endpoint_id: endpointInfo.id,
event_type: eventType,
endpoint: endpointInfo.path,
method: endpointInfo.method,
status_code: eventType === "api_call" ? 200 : 500,
response_time_ms: metadata.response_time_ms || 0,
metadata
})
}
);
if (!response.ok) {
console.error(
"[A402] Failed to record analytics:",
await response.text()
);
}
} catch (error) {
console.error("[A402] Record analytics error:", error);
}
}
};
function createA402Middleware(config) {
const sdk = new A402SDK(config);
return sdk;
}
var index_default = A402SDK;
export { A402SDK, createA402Middleware, index_default as default };
//# sourceMappingURL=index.mjs.map
//# sourceMappingURL=index.mjs.map