@carts1024/velo-sdk 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +213 -0
- package/package.json +18 -0
- package/src/client.test.ts +383 -0
- package/src/client.ts +93 -0
- package/src/errors.ts +95 -0
- package/src/http.ts +123 -0
- package/src/index.ts +4 -0
- package/src/testnet-flow.ts +152 -0
- package/src/types.ts +123 -0
- package/src/webhooks.test.ts +185 -0
- package/src/webhooks.ts +97 -0
- package/tsconfig.json +11 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
VeloConfig,
|
|
3
|
+
CreateCheckoutSessionParams,
|
|
4
|
+
RequestOptions,
|
|
5
|
+
PaymentIntent,
|
|
6
|
+
ListPaymentIntentsQuery,
|
|
7
|
+
ListResponse,
|
|
8
|
+
WebhookEvent,
|
|
9
|
+
VerifyWebhookParams,
|
|
10
|
+
} from "./types.ts";
|
|
11
|
+
|
|
12
|
+
import { HttpClient } from "./http.ts";
|
|
13
|
+
import { verifyWebhookSignature } from "./webhooks.ts";
|
|
14
|
+
|
|
15
|
+
export class Velo {
|
|
16
|
+
private readonly http: HttpClient;
|
|
17
|
+
|
|
18
|
+
static readonly webhooks = {
|
|
19
|
+
verify: async (params: VerifyWebhookParams): Promise<WebhookEvent> => {
|
|
20
|
+
return verifyWebhookSignature(params);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
readonly webhooks = {
|
|
25
|
+
verify: async (params: VerifyWebhookParams): Promise<WebhookEvent> => {
|
|
26
|
+
return verifyWebhookSignature(params);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
constructor(config: VeloConfig) {
|
|
31
|
+
if (
|
|
32
|
+
!config ||
|
|
33
|
+
!config.apiKey ||
|
|
34
|
+
typeof config.apiKey !== "string" ||
|
|
35
|
+
config.apiKey.trim() === ""
|
|
36
|
+
) {
|
|
37
|
+
throw new Error("API key is required");
|
|
38
|
+
}
|
|
39
|
+
this.http = new HttpClient(config);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
readonly checkout = {
|
|
43
|
+
sessions: {
|
|
44
|
+
create: async (
|
|
45
|
+
params: CreateCheckoutSessionParams,
|
|
46
|
+
options?: RequestOptions,
|
|
47
|
+
): Promise<PaymentIntent> => {
|
|
48
|
+
return this.http.request<PaymentIntent>("POST", "/api/v1/payment-intents", params, options);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
readonly paymentIntents = {
|
|
54
|
+
create: async (
|
|
55
|
+
params: CreateCheckoutSessionParams,
|
|
56
|
+
options?: RequestOptions,
|
|
57
|
+
): Promise<PaymentIntent> => {
|
|
58
|
+
return this.http.request<PaymentIntent>("POST", "/api/v1/payment-intents", params, options);
|
|
59
|
+
},
|
|
60
|
+
retrieve: async (id: string, options?: RequestOptions): Promise<PaymentIntent> => {
|
|
61
|
+
if (!id || typeof id !== "string" || id.trim() === "") {
|
|
62
|
+
throw new Error("Payment intent ID is required");
|
|
63
|
+
}
|
|
64
|
+
return this.http.request<PaymentIntent>(
|
|
65
|
+
"GET",
|
|
66
|
+
`/api/v1/payment-intents/${encodeURIComponent(id)}`,
|
|
67
|
+
undefined,
|
|
68
|
+
options,
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
list: async (
|
|
72
|
+
query?: ListPaymentIntentsQuery,
|
|
73
|
+
options?: RequestOptions,
|
|
74
|
+
): Promise<ListResponse<PaymentIntent>> => {
|
|
75
|
+
let path = "/api/v1/payment-intents";
|
|
76
|
+
const searchParams = new URLSearchParams();
|
|
77
|
+
if (query?.status) {
|
|
78
|
+
searchParams.append("status", query.status);
|
|
79
|
+
}
|
|
80
|
+
if (query?.limit !== undefined) {
|
|
81
|
+
searchParams.append("limit", String(query.limit));
|
|
82
|
+
}
|
|
83
|
+
if (query?.cursor) {
|
|
84
|
+
searchParams.append("cursor", query.cursor);
|
|
85
|
+
}
|
|
86
|
+
const queryString = searchParams.toString();
|
|
87
|
+
if (queryString) {
|
|
88
|
+
path += `?${queryString}`;
|
|
89
|
+
}
|
|
90
|
+
return this.http.request<ListResponse<PaymentIntent>>("GET", path, undefined, options);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export class VeloError extends Error {
|
|
2
|
+
readonly status?: number;
|
|
3
|
+
readonly code?: string;
|
|
4
|
+
readonly param?: string;
|
|
5
|
+
readonly requestId?: string;
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
message: string,
|
|
9
|
+
options?: { status?: number; code?: string; param?: string; requestId?: string },
|
|
10
|
+
) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = this.constructor.name;
|
|
13
|
+
this.status = options?.status;
|
|
14
|
+
this.code = options?.code;
|
|
15
|
+
this.param = options?.param;
|
|
16
|
+
this.requestId = options?.requestId;
|
|
17
|
+
|
|
18
|
+
if (Error.captureStackTrace) {
|
|
19
|
+
Error.captureStackTrace(this, this.constructor);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class VeloAuthError extends VeloError {
|
|
25
|
+
constructor(message: string, options?: { status?: number; code?: string; requestId?: string }) {
|
|
26
|
+
super(message, options);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class VeloValidationError extends VeloError {
|
|
31
|
+
constructor(
|
|
32
|
+
message: string,
|
|
33
|
+
options?: { status?: number; code?: string; param?: string; requestId?: string },
|
|
34
|
+
) {
|
|
35
|
+
super(message, options);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class VeloRateLimitError extends VeloError {
|
|
40
|
+
constructor(message: string, options?: { status?: number; code?: string; requestId?: string }) {
|
|
41
|
+
super(message, options);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class VeloAPIError extends VeloError {
|
|
46
|
+
constructor(message: string, options?: { status?: number; code?: string; requestId?: string }) {
|
|
47
|
+
super(message, options);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class VeloWebhookSignatureVerificationError extends VeloValidationError {
|
|
52
|
+
constructor(message: string) {
|
|
53
|
+
super(message, { status: 400, code: "webhook_signature_verification_failed" });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function mapErrorResponse(status: number, payload: unknown, requestId?: string): VeloError {
|
|
58
|
+
const errorObj =
|
|
59
|
+
payload &&
|
|
60
|
+
typeof payload === "object" &&
|
|
61
|
+
"error" in payload &&
|
|
62
|
+
payload.error &&
|
|
63
|
+
typeof payload.error === "object"
|
|
64
|
+
? (payload.error as Record<string, unknown>)
|
|
65
|
+
: {};
|
|
66
|
+
const message =
|
|
67
|
+
typeof errorObj.message === "string"
|
|
68
|
+
? errorObj.message
|
|
69
|
+
: `Request failed with status ${status}`;
|
|
70
|
+
const code = typeof errorObj.code === "string" ? errorObj.code : undefined;
|
|
71
|
+
const param = typeof errorObj.param === "string" ? errorObj.param : undefined;
|
|
72
|
+
const reqId = typeof errorObj.requestId === "string" ? errorObj.requestId : requestId;
|
|
73
|
+
const errorType = typeof errorObj.type === "string" ? errorObj.type : undefined;
|
|
74
|
+
|
|
75
|
+
const options = { status, code, param, requestId: reqId };
|
|
76
|
+
|
|
77
|
+
if (status === 401 || errorType === "auth_error") {
|
|
78
|
+
return new VeloAuthError(message, options);
|
|
79
|
+
}
|
|
80
|
+
if (status === 429 || errorType === "rate_limit_error") {
|
|
81
|
+
return new VeloRateLimitError(message, options);
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
status === 400 ||
|
|
85
|
+
status === 404 ||
|
|
86
|
+
status === 409 ||
|
|
87
|
+
errorType === "validation_error" ||
|
|
88
|
+
errorType === "not_found_error" ||
|
|
89
|
+
errorType === "idempotency_error"
|
|
90
|
+
) {
|
|
91
|
+
return new VeloValidationError(message, options);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return new VeloAPIError(message, options);
|
|
95
|
+
}
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { VeloConfig, RequestOptions } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
import { mapErrorResponse, VeloAPIError, VeloRateLimitError } from "./errors.ts";
|
|
4
|
+
|
|
5
|
+
export function resolveBaseUrl(config: VeloConfig): string {
|
|
6
|
+
if (config.baseUrl) {
|
|
7
|
+
return config.baseUrl;
|
|
8
|
+
}
|
|
9
|
+
const env = config.environment;
|
|
10
|
+
if (env === "production") {
|
|
11
|
+
return "https://api.velo.pay";
|
|
12
|
+
}
|
|
13
|
+
if (env === "testnet") {
|
|
14
|
+
return "https://api.testnet.velo.pay";
|
|
15
|
+
}
|
|
16
|
+
return "http://localhost:3000";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class HttpClient {
|
|
20
|
+
private readonly apiKey: string;
|
|
21
|
+
private readonly baseUrl: string;
|
|
22
|
+
private readonly timeoutMs: number;
|
|
23
|
+
|
|
24
|
+
constructor(config: VeloConfig) {
|
|
25
|
+
this.apiKey = config.apiKey;
|
|
26
|
+
this.baseUrl = resolveBaseUrl(config);
|
|
27
|
+
this.timeoutMs = config.timeoutMs ?? 30000;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async request<T>(
|
|
31
|
+
method: "GET" | "POST" | "PUT" | "DELETE",
|
|
32
|
+
path: string,
|
|
33
|
+
body?: unknown,
|
|
34
|
+
options?: RequestOptions,
|
|
35
|
+
): Promise<T> {
|
|
36
|
+
const url = `${this.baseUrl}${path}`;
|
|
37
|
+
const headers: Record<string, string> = {
|
|
38
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
39
|
+
"Content-Type": "application/json",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
if (options?.idempotencyKey) {
|
|
43
|
+
headers["Idempotency-Key"] = options.idempotencyKey;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const maxRetries = 2;
|
|
47
|
+
let attempt = 0;
|
|
48
|
+
|
|
49
|
+
while (true) {
|
|
50
|
+
attempt++;
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
const timeoutId = setTimeout(() => {
|
|
53
|
+
controller.abort();
|
|
54
|
+
}, this.timeoutMs);
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const response = await fetch(url, {
|
|
58
|
+
method,
|
|
59
|
+
headers,
|
|
60
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const requestId = response.headers.get("x-request-id") || undefined;
|
|
65
|
+
|
|
66
|
+
let payload: unknown;
|
|
67
|
+
const contentType = response.headers.get("content-type") || "";
|
|
68
|
+
if (contentType.includes("application/json")) {
|
|
69
|
+
const text = await response.text();
|
|
70
|
+
try {
|
|
71
|
+
payload = JSON.parse(text);
|
|
72
|
+
} catch {
|
|
73
|
+
payload = text;
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
payload = await response.text();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
throw mapErrorResponse(response.status, payload, requestId);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return payload as T;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (
|
|
86
|
+
err instanceof Error &&
|
|
87
|
+
(err.name === "AbortError" || (err instanceof DOMException && err.name === "AbortError"))
|
|
88
|
+
) {
|
|
89
|
+
throw new VeloAPIError(`Request timed out after ${this.timeoutMs}ms`, {
|
|
90
|
+
status: 408,
|
|
91
|
+
code: "timeout",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Network failures in fetch throw TypeError
|
|
96
|
+
const isNetworkError =
|
|
97
|
+
err instanceof TypeError ||
|
|
98
|
+
(err instanceof Error &&
|
|
99
|
+
(err.message.includes("fetch failed") ||
|
|
100
|
+
err.message.includes("ECONNREFUSED") ||
|
|
101
|
+
err.message.includes("ENOTFOUND") ||
|
|
102
|
+
err.message.includes("network error")));
|
|
103
|
+
|
|
104
|
+
const isRetryableError =
|
|
105
|
+
err instanceof VeloRateLimitError ||
|
|
106
|
+
(err instanceof VeloAPIError && err.status !== undefined && err.status >= 500) ||
|
|
107
|
+
isNetworkError;
|
|
108
|
+
|
|
109
|
+
const canRetryMethod = method === "GET" || (method === "POST" && !!options?.idempotencyKey);
|
|
110
|
+
|
|
111
|
+
if (isRetryableError && canRetryMethod && attempt <= maxRetries) {
|
|
112
|
+
const delay = 500 * Math.pow(2, attempt - 1);
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
throw err;
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(timeoutId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
import { Velo } from "./client.ts";
|
|
5
|
+
|
|
6
|
+
const PROJECT_ID = "j97102bbzrfdaabs3s05g1624h89k054";
|
|
7
|
+
|
|
8
|
+
async function run() {
|
|
9
|
+
console.log("🚀 Starting Velo SDK E2E local verification flow...");
|
|
10
|
+
|
|
11
|
+
// 1. Generate a temporary API Key using Convex CLI
|
|
12
|
+
console.log("Generating a temporary API key for project demopay...");
|
|
13
|
+
const rawOutput = execSync(
|
|
14
|
+
`npx convex run projects/mutation:generateApiKeyInternal '{"id": "${PROJECT_ID}", "label": "E2E SDK Test"}'`,
|
|
15
|
+
{ cwd: "../../apps/web", encoding: "utf8" },
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// Extract token using regex matching tk_live_[a-f0-9]{32}
|
|
19
|
+
const match = rawOutput.match(/tk_live_[a-f0-9]{32}/);
|
|
20
|
+
if (!match) {
|
|
21
|
+
throw new Error(`Failed to extract API key from output: ${rawOutput}`);
|
|
22
|
+
}
|
|
23
|
+
const apiKey = match[0];
|
|
24
|
+
console.log(`Generated API Key: ${apiKey.slice(0, 12)}...`);
|
|
25
|
+
|
|
26
|
+
// 2. Initialize Velo client
|
|
27
|
+
const velo = new Velo({
|
|
28
|
+
apiKey,
|
|
29
|
+
environment: "testnet",
|
|
30
|
+
baseUrl: "http://localhost:3000",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 3. Create a checkout session
|
|
34
|
+
console.log("Creating checkout session...");
|
|
35
|
+
const session = await velo.checkout.sessions.create({
|
|
36
|
+
amount: "10.00",
|
|
37
|
+
asset: "USDC",
|
|
38
|
+
description: "SDK Verification Order #1001",
|
|
39
|
+
successUrl: "http://localhost:3000/pay/success",
|
|
40
|
+
cancelUrl: "http://localhost:3000/pay/cancel",
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log("Checkout session created successfully:");
|
|
44
|
+
console.log(`- ID: ${session.id}`);
|
|
45
|
+
console.log(`- Status: ${session.status}`);
|
|
46
|
+
console.log(`- Checkout URL: ${session.checkoutUrl}`);
|
|
47
|
+
|
|
48
|
+
if (!session.checkoutUrl || !session.checkoutUrl.startsWith("http://localhost:3000/pay/")) {
|
|
49
|
+
throw new Error("Invalid checkout URL format");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 4. Retrieve the payment intent using the SDK
|
|
53
|
+
console.log(`Retrieving payment intent ${session.id}...`);
|
|
54
|
+
const retrieved = await velo.paymentIntents.retrieve(session.id);
|
|
55
|
+
console.log(`Retrieved status: ${retrieved.status}`);
|
|
56
|
+
if (retrieved.status !== "created") {
|
|
57
|
+
throw new Error(`Expected status to be 'created', got: ${retrieved.status}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 5. Transition to pending using Convex CLI
|
|
61
|
+
console.log("Simulating customer opening the checkout (transition to pending)...");
|
|
62
|
+
execSync(
|
|
63
|
+
`npx convex run payment_intents/mutations:updateStatus '{"paymentIntentId": "${session.id}", "status": "pending"}'`,
|
|
64
|
+
{ cwd: "../../apps/web" },
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const pendingRetrieved = await velo.paymentIntents.retrieve(session.id);
|
|
68
|
+
console.log(`Retrieved status after opening checkout: ${pendingRetrieved.status}`);
|
|
69
|
+
if (pendingRetrieved.status !== "pending") {
|
|
70
|
+
throw new Error(`Expected status to be 'pending', got: ${pendingRetrieved.status}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 6. Transition to paid using Convex CLI (simulates ledger verification success)
|
|
74
|
+
console.log("Simulating on-chain payment success (transition to paid)...");
|
|
75
|
+
execSync(
|
|
76
|
+
`npx convex run payment_intents/mutations:markVerifiedPaid '{"paymentIntentId": "${session.id}", "txHash": "c8731ea2a4fd43d0221b4c0eb5687c098e6bfcb6a58dbb4d01d698a0a8064cd9"}'`,
|
|
77
|
+
{ cwd: "../../apps/web" },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const paidRetrieved = await velo.paymentIntents.retrieve(session.id);
|
|
81
|
+
console.log(`Retrieved status after payment verified: ${paidRetrieved.status}`);
|
|
82
|
+
if (paidRetrieved.status !== "paid") {
|
|
83
|
+
throw new Error(`Expected status to be 'paid', got: ${paidRetrieved.status}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 7. Verify listing payment intents
|
|
87
|
+
console.log("Listing payment intents...");
|
|
88
|
+
const list = await velo.paymentIntents.list({ limit: 5 });
|
|
89
|
+
console.log(`Found ${list.data.length} payment intents in list.`);
|
|
90
|
+
const found = list.data.some((item) => item.id === session.id);
|
|
91
|
+
if (!found) {
|
|
92
|
+
throw new Error("Created session not found in list response");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 8. Simulate webhook verification
|
|
96
|
+
console.log("Simulating and verifying webhook signature...");
|
|
97
|
+
const secret = "whsec_48d83b56a6ffd001b6cb3a75975a9254";
|
|
98
|
+
const payloadObj = {
|
|
99
|
+
id: crypto.randomUUID(),
|
|
100
|
+
type: "payment.succeeded",
|
|
101
|
+
test: true,
|
|
102
|
+
sentAt: new Date().toISOString(),
|
|
103
|
+
project: {
|
|
104
|
+
id: PROJECT_ID,
|
|
105
|
+
name: "DemoPay",
|
|
106
|
+
slug: "demopay",
|
|
107
|
+
},
|
|
108
|
+
paymentIntent: {
|
|
109
|
+
id: session.id,
|
|
110
|
+
amount: "10.00",
|
|
111
|
+
asset: "USDC",
|
|
112
|
+
status: "paid",
|
|
113
|
+
createdAt: new Date().toISOString(),
|
|
114
|
+
updatedAt: new Date().toISOString(),
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const payload = JSON.stringify(payloadObj);
|
|
119
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
120
|
+
const signaturePayload = `${timestamp}.${payload}`;
|
|
121
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
122
|
+
hmac.update(signaturePayload);
|
|
123
|
+
const hash = hmac.digest("hex");
|
|
124
|
+
const signatureHeader = `t=${timestamp},v1=${hash}`;
|
|
125
|
+
|
|
126
|
+
const verifiedEvent = await Velo.webhooks.verify({
|
|
127
|
+
payload,
|
|
128
|
+
signature: signatureHeader,
|
|
129
|
+
secret,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (verifiedEvent.type === "payment.succeeded") {
|
|
133
|
+
console.log("Webhook verified successfully:");
|
|
134
|
+
console.log(`- Event ID: ${verifiedEvent.id}`);
|
|
135
|
+
console.log(`- Event Type: ${verifiedEvent.type}`);
|
|
136
|
+
console.log(`- Payment Intent ID: ${verifiedEvent.paymentIntent.id}`);
|
|
137
|
+
console.log(`- Payment Intent Status: ${verifiedEvent.paymentIntent.status}`);
|
|
138
|
+
|
|
139
|
+
if (verifiedEvent.paymentIntent.status !== "paid") {
|
|
140
|
+
throw new Error("Webhook verification returned incorrect status");
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
throw new Error(`Expected payment.succeeded event, got: ${verifiedEvent.type}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log("🎉 Velo SDK local e2e integration flow completed successfully!");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
run().catch((err) => {
|
|
150
|
+
console.error("❌ E2E verification failed:", err);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export type PaymentIntentStatus =
|
|
2
|
+
| "created"
|
|
3
|
+
| "pending"
|
|
4
|
+
| "paid"
|
|
5
|
+
| "failed"
|
|
6
|
+
| "expired"
|
|
7
|
+
| "cancelled";
|
|
8
|
+
|
|
9
|
+
export type PaymentIntent = {
|
|
10
|
+
id: string;
|
|
11
|
+
object: "payment_intent";
|
|
12
|
+
paymentIntentId: string;
|
|
13
|
+
status: PaymentIntentStatus;
|
|
14
|
+
amount: string;
|
|
15
|
+
asset: string;
|
|
16
|
+
description: string | null;
|
|
17
|
+
checkoutUrl: string | null;
|
|
18
|
+
successUrl: string | null;
|
|
19
|
+
cancelUrl: string | null;
|
|
20
|
+
expiresAt: string;
|
|
21
|
+
createdAt: string;
|
|
22
|
+
updatedAt: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type VeloConfig = {
|
|
26
|
+
apiKey: string;
|
|
27
|
+
baseUrl?: string;
|
|
28
|
+
environment?: "production" | "testnet" | "development" | string;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type CreateCheckoutSessionParams = {
|
|
33
|
+
amount: string;
|
|
34
|
+
asset?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
successUrl?: string;
|
|
37
|
+
cancelUrl?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type RequestOptions = {
|
|
41
|
+
idempotencyKey?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type ListPaymentIntentsQuery = {
|
|
45
|
+
status?: PaymentIntentStatus;
|
|
46
|
+
limit?: number;
|
|
47
|
+
cursor?: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type ListResponse<T> = {
|
|
51
|
+
object: "list";
|
|
52
|
+
data: T[];
|
|
53
|
+
hasMore: boolean;
|
|
54
|
+
nextCursor: string | null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type WebhookEventType =
|
|
58
|
+
| "payment.created"
|
|
59
|
+
| "payment.succeeded"
|
|
60
|
+
| "payment.failed"
|
|
61
|
+
| "payment_access.activated"
|
|
62
|
+
| "payment.access_activated"
|
|
63
|
+
| "contract.event";
|
|
64
|
+
|
|
65
|
+
export type WebhookEventBase = {
|
|
66
|
+
id: string;
|
|
67
|
+
type: WebhookEventType;
|
|
68
|
+
test: boolean;
|
|
69
|
+
sentAt: string;
|
|
70
|
+
project: {
|
|
71
|
+
id: string;
|
|
72
|
+
registryProjectId: string;
|
|
73
|
+
name: string;
|
|
74
|
+
slug: string;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type WebhookPaymentIntentData = {
|
|
79
|
+
id: string;
|
|
80
|
+
amount: string;
|
|
81
|
+
asset: string;
|
|
82
|
+
receiverAddress: string;
|
|
83
|
+
merchantName: string;
|
|
84
|
+
description: string | null;
|
|
85
|
+
status: PaymentIntentStatus;
|
|
86
|
+
payerAddress?: string;
|
|
87
|
+
txHash?: string;
|
|
88
|
+
createdAt: string;
|
|
89
|
+
updatedAt: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type WebhookPaymentEvent = WebhookEventBase & {
|
|
93
|
+
type:
|
|
94
|
+
| "payment.created"
|
|
95
|
+
| "payment.succeeded"
|
|
96
|
+
| "payment.failed"
|
|
97
|
+
| "payment_access.activated"
|
|
98
|
+
| "payment.access_activated";
|
|
99
|
+
paymentIntent: WebhookPaymentIntentData;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type WebhookContractEvent = WebhookEventBase & {
|
|
103
|
+
type: "contract.event";
|
|
104
|
+
contractId: string;
|
|
105
|
+
transactionHash: string;
|
|
106
|
+
ledger: number;
|
|
107
|
+
event: {
|
|
108
|
+
id: string;
|
|
109
|
+
topic: string;
|
|
110
|
+
type: string;
|
|
111
|
+
data: unknown;
|
|
112
|
+
observedAt: string;
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type WebhookEvent = WebhookPaymentEvent | WebhookContractEvent;
|
|
117
|
+
|
|
118
|
+
export type VerifyWebhookParams = {
|
|
119
|
+
payload: string;
|
|
120
|
+
signature: string | null;
|
|
121
|
+
secret: string;
|
|
122
|
+
toleranceSeconds?: number;
|
|
123
|
+
};
|