Created
September 23, 2025 04:34
-
-
Save arnm/9ffb8e18b0c454e3eb5ae1dd277701b2 to your computer and use it in GitHub Desktop.
wise-tht-2025
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const axios = require("axios"); | |
| const { v4: uuidv4 } = require("uuid"); | |
| const BASE_URL = "https://api.sandbox.transferwise.tech"; | |
| const TOKEN = process.env.WISE_API_TOKEN; | |
| class WiseApiError extends Error { | |
| constructor(message, { status, traceId, code, details, originalError } = {}) { | |
| super(message); | |
| this.name = "WiseApiError"; | |
| this.status = status; | |
| this.traceId = traceId; | |
| this.code = code; | |
| this.details = details; | |
| this.originalError = originalError; | |
| } | |
| static fromAxios(err, context = "Wise API") { | |
| if (err.response) { | |
| // Server responded with error status (2xx range) | |
| const status = err.response.status; | |
| const traceId = err.response.headers?.["x-trace-id"]; | |
| const data = err.response.data; | |
| const code = data?.errors?.[0]?.code || data?.code; | |
| const message = `${context} failed with status ${status}`; | |
| return new WiseApiError(message, { status, traceId, code, details: data, originalError: err }); | |
| } else if (err.request) { | |
| // Request was made but no response received (network issues, timeouts) | |
| const message = `${context} failed - no response received`; | |
| return new WiseApiError(message, { code: err.code, details: { request: "No response received" }, originalError: err }); | |
| } else { | |
| // Error in request setup | |
| const message = `${context} failed - request setup error: ${err.message}`; | |
| return new WiseApiError(message, { details: { setup: err.message }, originalError: err }); | |
| } | |
| } | |
| } | |
| class WiseClient { | |
| constructor({ token = TOKEN, baseURL = BASE_URL } = {}) { | |
| if (!token) throw new Error("Missing API token. Set WISE_API_TOKEN."); | |
| this.http = axios.create({ | |
| baseURL, | |
| headers: { | |
| Authorization: `Bearer ${token}`, | |
| "Content-Type": "application/json", | |
| }, | |
| timeout: 30000, | |
| }); | |
| } | |
| handleError(context, error) { | |
| if (axios.isAxiosError(error)) { | |
| throw WiseApiError.fromAxios(error, context); | |
| } | |
| throw new WiseApiError(`${context} encountered a non-HTTP error`, { details: { message: error?.message } }); | |
| } | |
| async listProfiles() { | |
| try { | |
| const res = await this.http.get("/v2/profiles"); | |
| return res.data; | |
| } catch (error) { | |
| this.handleError("List Profiles", error); | |
| } | |
| } | |
| async createQuote(profileId, { sourceCurrency, targetCurrency, sourceAmount } = {}) { | |
| try { | |
| const res = await this.http.post(`/v3/profiles/${profileId}/quotes`, { | |
| sourceCurrency, | |
| targetCurrency, | |
| sourceAmount, | |
| }); | |
| return res.data; | |
| } catch (error) { | |
| this.handleError("Create Quote", error); | |
| } | |
| } | |
| async createRecipient(profileId, { currency, type, accountHolderName, details } = {}) { | |
| try { | |
| const res = await this.http.post("/v1/accounts", { | |
| currency, | |
| type, | |
| profile: profileId, | |
| accountHolderName, | |
| details, | |
| }); | |
| return res.data; | |
| } catch (error) { | |
| this.handleError("Create Recipient", error); | |
| } | |
| } | |
| async createTransfer(quoteUuid, targetAccount, { reference, customerTransactionId } = {}) { | |
| try { | |
| const res = await this.http.post("/v1/transfers", { | |
| targetAccount, | |
| quoteUuid, | |
| customerTransactionId: customerTransactionId || uuidv4(), | |
| details: { reference }, | |
| }); | |
| return res.data; | |
| } catch (error) { | |
| this.handleError("Create Transfer", error); | |
| } | |
| } | |
| } | |
| function findPersonalProfileId(profiles) { | |
| if (!Array.isArray(profiles) || profiles.length === 0) return null; | |
| const personal = profiles.find((p) => p.type === "PERSONAL"); | |
| return (personal ?? profiles[0]).id; | |
| } | |
| function selectBankTransferOption(quote) { | |
| const opts = quote?.paymentOptions || []; | |
| return opts.find( | |
| (o) => o.payIn === "BANK_TRANSFER" && o.payOut === "BANK_TRANSFER" | |
| ) || null; | |
| } | |
| function getTargetAmount(quote, bankOption) { | |
| return bankOption?.targetAmount ?? quote?.targetAmount ?? null; | |
| } | |
| function getTargetCurrency(quote, bankOption) { | |
| return bankOption?.targetCurrency ?? quote?.targetCurrency ?? "GBP"; | |
| } | |
| function getExchangeRateStr(quote) { | |
| const rate = quote?.rate; | |
| return rate !== undefined && rate !== null && !Number.isNaN(parseFloat(rate)) | |
| ? Number(rate).toFixed(4) | |
| : "N/A"; | |
| } | |
| function getFeesDisplay(quote, bankOption) { | |
| const feeAmount = bankOption?.fee?.total; | |
| const feeCurrency = bankOption?.price?.total?.value?.currency || quote?.sourceCurrency; | |
| return feeAmount != null ? `${feeAmount} ${feeCurrency || ""}`.trim() : "N/A"; | |
| } | |
| function getDeliveryEstimate(bankOption) { | |
| return bankOption?.estimatedDelivery | |
| ? new Date(bankOption.estimatedDelivery).toLocaleString() | |
| : (bankOption?.formattedEstimatedDelivery || "N/A"); | |
| } | |
| const runLogic = async () => { | |
| const client = new WiseClient({}); | |
| // Task 1: Find out the Personal Profile ID of the user. | |
| const profiles = await client.listProfiles(); | |
| const profileId = findPersonalProfileId(profiles); | |
| if (!profileId) throw new Error("No profile found on account"); | |
| console.log(`Profile ID: ${profileId}`); // Example Console Log | |
| // Create Quote | |
| const quote = await client.createQuote(profileId, { | |
| sourceCurrency: "SGD", | |
| targetCurrency: "GBP", | |
| sourceAmount: 1000, | |
| }); | |
| // [IMP] Select BANK_TRANSFER option for both payin and payout | |
| // Make sure you are selecting the correct payin and payout options to get the correct transfer fee. | |
| // Task 2: Console Log the Quote ID | |
| // Task 3: Console Log the Amount the recipient will receive, including the currency (e.g. "12.34 GBP") | |
| // Task 4: Console Log the Exchange Rate (4 decimal places, e.g. "1.2345") | |
| // Task 5: Console Log the Fees (total fee) | |
| // Task 6: Console Log the Delivery Estimates (human readable format) | |
| const bankOption = selectBankTransferOption(quote); | |
| if (!bankOption) { | |
| throw new Error("BANK_TRANSFER payment option not found in quote."); | |
| } | |
| console.log(`Quote ID: ${quote.id}`); | |
| const targetAmount = getTargetAmount(quote, bankOption); | |
| const targetCurrency = getTargetCurrency(quote, bankOption); | |
| console.log(`Amount (recipient receives): ${targetAmount} ${targetCurrency}`); | |
| const rateStr = getExchangeRateStr(quote); | |
| console.log(`Exchange Rate: ${rateStr}`); | |
| const feesDisplay = getFeesDisplay(quote, bankOption); | |
| console.log(`Fees: ${feesDisplay}`); | |
| const deliveryHuman = getDeliveryEstimate(bankOption); | |
| console.log(`Delivery Estimate: ${deliveryHuman}`); | |
| // Create Recipient (GBP Sort Code) | |
| const recipient = await client.createRecipient(profileId, { | |
| currency: "GBP", | |
| type: "sort_code", | |
| accountHolderName: "GBP Person Name", | |
| details: { | |
| legalType: "PRIVATE", | |
| sortCode: "04-00-04", | |
| accountNumber: "12345678", | |
| }, | |
| }); | |
| // Task 7: Console Log the Recipient ID | |
| console.log(`Recipient ID: ${recipient.id}`); | |
| // Create Transfer | |
| const transfer = await client.createTransfer(quote.id, recipient.id, { reference: "Payment reference" }); | |
| // Task 8: Console Log the Transfer ID | |
| console.log(`Transfer ID: ${transfer.id}`); | |
| // Task 9: Console Log the Transfer Status | |
| console.log(`Transfer Status: ${transfer.status}`); | |
| // Remember to copy all the console logs to a text file for submission. | |
| console.log("All tasks completed successfully."); | |
| }; | |
| Promise.resolve() | |
| .then(() => runLogic()) | |
| .catch((error) => { | |
| if (error instanceof WiseApiError) { | |
| console.error("An error occurred"); | |
| if (error.status) console.error(`Status ${error.status}`); | |
| if (error.traceId) console.error(`Trace ID: ${error.traceId}`); | |
| if (error.code) console.error(`Code: ${error.code}`); | |
| if (error.details) console.error(error.details); | |
| } else { | |
| console.error("An unexpected error occurred"); | |
| console.error(error?.message || error); | |
| } | |
| }); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Output