UNPKG

@banksnussman/venmo

Version:

Interact with Venmo from Typescript

519 lines (511 loc) 18.9 kB
"use strict"; var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropDescs = Object.getOwnPropertyDescriptors; var __getOwnPropNames = Object.getOwnPropertyNames; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __async = (__this, __arguments, generator) => { return new Promise((resolve, reject) => { var fulfilled = (value) => { try { step(generator.next(value)); } catch (e) { reject(e); } }; var rejected = (value) => { try { step(generator.throw(value)); } catch (e) { reject(e); } }; var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); step((generator = generator.apply(__this, __arguments)).next()); }); }; // src/index.ts var src_exports = {}; __export(src_exports, { Venmo: () => Venmo }); module.exports = __toCommonJS(src_exports); var import_config = require("dotenv/config"); var import_graphql_request3 = require("graphql-request"); var import_uuid2 = require("uuid"); // src/constants.ts var import_uuid = require("uuid"); var USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36"; var DEVICE_ID = `fp01-${(0, import_uuid.v4)()}`; var GRAPHQL_ENDPOINT = "https://api.venmo.com/graphql"; // src/graphql/funding.ts var import_graphql_request = require("graphql-request"); var FundingInstrumentsQuery = import_graphql_request.gql` query getUserFundingInstruments { profile { ... on Profile { identity { ... on Identity { capabilities __typename } __typename } wallet { id assets { logoThumbnail __typename } instrumentType name fees { feeType fixedAmount variablePercentage __typename } metadata { ...BalanceMetadata ... on BankFundingInstrumentMetadata { bankName isVerified lastFourDigits uniqueIdentifier __typename } ... on CardFundingInstrumentMetadata { issuerName lastFourDigits networkName isVenmoCard expirationDate expirationStatus quasiCash __typename } __typename } roles { merchantPayments peerPayments __typename } __typename } __typename } __typename } } fragment BalanceMetadata on BalanceFundingInstrumentMetadata { availableBalance { value transactionType displayString __typename } __typename } `; // src/graphql/people.ts var import_graphql_request2 = require("graphql-request"); var PeopleQuery = import_graphql_request2.gql` query People($input: SearchInput!) { search(input: $input) { people { edges { node { displayName id type avatar { url } handle firstName lastName isFriend } } } } } `; // src/pay.ts var import_playwright = require("playwright"); function payment(options, credentials) { return __async(this, null, function* () { const browser = yield import_playwright.chromium.launch(); const page = yield browser.newPage(); yield page.goto("https://account.venmo.com/"); yield page.waitForLoadState("networkidle"); const pageTitle = yield page.title(); if (pageTitle.includes("Sign in")) { const isUsernameRemebered = yield page.isVisible(`text='${credentials.username}'`); if (!isUsernameRemebered) { yield page.getByLabel("Enter email, mobile, or username").click(); yield page.getByLabel("Enter email, mobile, or username").fill(credentials.username); yield page.getByLabel("Enter email, mobile, or username").press("Enter"); } yield page.getByLabel("Password", { exact: true }).click(); yield page.getByLabel("Password", { exact: true }).fill(credentials.password); yield page.getByLabel("Password", { exact: true }).press("Enter"); yield page.getByRole("link", { name: "Confirm another way" }).click(); yield page.getByLabel("Bank account number").click(); yield page.getByLabel("Bank account number").fill(credentials.bankAccountNumber); yield page.getByRole("button", { name: "Confirm it" }).click(); yield page.getByRole("button", { name: "Not now" }).click(); yield page.context().storageState({ path: "storage.json" }); } yield page.getByRole("link", { name: "Pay or Request" }).click(); yield page.getByPlaceholder("0").click(); yield page.getByPlaceholder("0").fill(String(options.amount)); yield page.getByPlaceholder("Name, @username, email, phone").click(); yield page.getByPlaceholder("Name, @username, email, phone").fill(options.username); yield page.waitForResponse((resp) => resp.url().includes("/graphql")), yield page.getByRole("option").first().click(); yield page.getByTestId("payment-note-input").click(); yield page.getByTestId("payment-note-input").fill(options.note); yield page.getByRole("button", { name: "Pay" }).click(); const balanceData = yield page.getByTestId("money").innerText(); const balance = Number(balanceData.split("$")[1]); yield page.getByRole("button", { name: "Pay" }).click(); yield page.context().storageState({ path: "storage.json" }); yield browser.close(); return balance - options.amount; }); } // src/index.ts var Venmo = class { constructor(credentials) { this.credentials = credentials; this.w_fc = (0, import_uuid2.v4)(); } login() { return __async(this, null, function* () { var _a, _b, _c, _d, _e, _f, _g, _h, _i; const loginResult = yield fetch( "https://venmo.com/login", { headers: { "user-agent": USER_AGENT, "Cookie": `v_id=${DEVICE_ID}`, "Content-Type": "application/json" }, method: "POST", body: JSON.stringify({ phoneEmailUsername: this.credentials.username, password: this.credentials.password, return_json: "true" }) } ); if (loginResult.status !== 401) { throw new Error(`Recieved unexpected status code ${loginResult.status} on initial POST to login`); } const data = yield loginResult.json(); if (data.error.message !== "Additional authentication is required") { throw new Error(`Got unexpcted initial login error. Expcted mfa error.`); } const otpSecret = loginResult.headers.get("venmo-otp-secret"); if (!otpSecret) { throw new Error("Unable to get opt secret from inital login headers"); } const verifyBankResult = yield fetch( `https://account.venmo.com/account/mfa/verify-bank?k=${otpSecret}`, { credentials: "include", headers: { "user-agent": USER_AGENT, "Cookie": `v_id=${DEVICE_ID}`, "Content-Type": "application/json" } } ); const rawResponseCookies = verifyBankResult.headers.get("set-cookie"); if (!rawResponseCookies) { throw new Error("Did not recieve neeeded cookies in the verify bank response."); } const csrfCookie = (_c = (_b = (_a = rawResponseCookies.split("_csrf=")) == null ? void 0 : _a[1]) == null ? void 0 : _b.split(";")) == null ? void 0 : _c[0]; if (!csrfCookie) { throw new Error("Unable to parse _csrf cookie"); } this.csrfCookie = csrfCookie; const verifyBankText = yield verifyBankResult.text(); const csrfData = (_d = verifyBankText.match(/<script id="__NEXT_DATA__" type="application\/json">([^<>]+)<\/script>/)) == null ? void 0 : _d[1]; if (!csrfData) { throw new Error("Unable to find next data that should contain our csrf token"); } const parsedNextData = JSON.parse(csrfData); const csrfToken = (_f = (_e = parsedNextData == null ? void 0 : parsedNextData["props"]) == null ? void 0 : _e["pageProps"]) == null ? void 0 : _f["csrfToken"]; if (!csrfToken) { throw new Error("Unable to find csrfToken within the nextjs data"); } this.csrfToken = csrfToken; const mfaHeaders = { "user-agent": USER_AGENT, "Cookie": `v_id=${DEVICE_ID}; _csrf=${csrfCookie}; login_email=${this.credentials.username};`, "csrf-token": csrfToken, "xsrf-token": csrfToken, "venmo-otp-secret": otpSecret, "Content-Type": "application/json" }; const finalSignInResult = yield fetch( "https://account.venmo.com/api/account/mfa/sign-in", { method: "POST", headers: mfaHeaders, body: JSON.stringify({ accountNumber: this.credentials.bankAccountNumber, isGroup: false }) } ); if (![200, 201].includes(finalSignInResult.status)) { throw new Error(`MFA Login was unsuccessful. Expected 200 status code but got ${finalSignInResult.status}`); } const rawCookies = finalSignInResult.headers.get("set-cookie"); if (!rawCookies) { throw new Error("Unable to read set-cookie from mfa login."); } const accessToken = (_i = (_h = (_g = rawCookies.split("api_access_token=")) == null ? void 0 : _g[1]) == null ? void 0 : _h.split(";")) == null ? void 0 : _i[0]; if (!accessToken) { throw new Error("Cookie api_access_token not found"); } this.accessToken = accessToken; const deviceResponse = yield fetch("https://account.venmo.com/api/device-data", { method: "POST", headers: { Cookie: `v_id=${DEVICE_ID}; _csrf=${this.csrfCookie}; api_access_token=${this.accessToken};`, "user-agent": USER_AGENT, "csrf-token": this.csrfToken, "xsrf-token": this.csrfToken, "Content-Type": "application/json" }, body: JSON.stringify({ correlationId: this.w_fc }) }); console.log("Device Data Cookie Response", deviceResponse.headers.get("set-cookie")); if (deviceResponse.status !== 200) { throw new Error(`Error while sending device data. Expected 200 but got ${deviceResponse.status}`); } return accessToken; }); } getIdentities() { return __async(this, null, function* () { if (!this.accessToken) { throw new Error("You are not authenticated. Maybe run login first."); } const result = yield fetch( "https://account.venmo.com/api/user/identities", { headers: { Cookie: `v_id=${DEVICE_ID}; w_fc=${this.w_fc}; _csrf=${this.csrfCookie}; api_access_token=${this.accessToken};`, "user-agent": USER_AGENT } } ); const data = yield result.json(); return data; }); } getStories(feedType, externalId) { return __async(this, null, function* () { if (!this.accessToken) { throw new Error("You are not authenticated. Maybe run login first."); } const result = yield fetch( `https://account.venmo.com/api/stories?feedType=${feedType}&externalId=${externalId}`, { headers: { Cookie: `v_id=${DEVICE_ID}; api_access_token=${this.accessToken};`, "user-agent": USER_AGENT } } ); const data = yield result.json(); return data; }); } getEligibility(eligibilityOptions) { return __async(this, null, function* () { if (!this.accessToken || !this.csrfToken || !this.csrfCookie || !this.w_fc) { throw new Error("You are not authenticated. Maybe run login first."); } const result = yield fetch( `https://account.venmo.com/api/eligibility`, { method: "POST", headers: { Cookie: `v_id=${DEVICE_ID}; w_fc=${this.w_fc}; _csrf=${this.csrfCookie}; api_access_token=${this.accessToken};`, "user-agent": USER_AGENT, "csrf-token": this.csrfToken, "xsrf-token": this.csrfToken, "Content-Type": "application/json" }, body: JSON.stringify(eligibilityOptions), credentials: "include" } ); const data = yield result.json(); return data; }); } getFundingInstruments() { return __async(this, null, function* () { const data = yield (0, import_graphql_request3.request)( GRAPHQL_ENDPOINT, FundingInstrumentsQuery, {}, { Authorization: `Bearer ${this.accessToken}`, "user-agent": USER_AGENT, "Venmo-Client-Id": "10", "Venmo-Device-Id": DEVICE_ID } ); return data; }); } getPerson(name) { return __async(this, null, function* () { var _a; const data = yield (0, import_graphql_request3.request)( GRAPHQL_ENDPOINT, PeopleQuery, { input: { name } }, { Authorization: `Bearer ${this.accessToken}`, "user-agent": USER_AGENT } ); return (_a = data.search.people.edges[0]) == null ? void 0 : _a.node; }); } brokenPay(paymentOptions) { return __async(this, null, function* () { var _a, _b, _c; if (!this.accessToken || !this.csrfToken || !this.csrfCookie || !this.w_fc) { throw new Error("You are not authenticated. Maybe run login first."); } console.log("csrfToken First", this.csrfToken); const paymentPageResult = yield fetch("https://account.venmo.com/pay", { credentials: "include", headers: { cookie: `v_id=${DEVICE_ID}; api_access_token=${this.accessToken}; _csrf=${this.csrfCookie}; w_fc=${this.w_fc};`, "user-agent": USER_AGENT } }); console.log("Payment Page Status", paymentPageResult.status, paymentPageResult.statusText); const verifyBankText = yield paymentPageResult.text(); const csrfData = (_a = verifyBankText.match(/<script id="__NEXT_DATA__" type="application\/json">([^<>]+)<\/script>/)) == null ? void 0 : _a[1]; if (!csrfData) { throw new Error("Unable to find next data that should contain our csrf token"); } const parsedNextData = JSON.parse(csrfData); const csrfToken = (_c = (_b = parsedNextData == null ? void 0 : parsedNextData["props"]) == null ? void 0 : _b["pageProps"]) == null ? void 0 : _c["csrfToken"]; if (!csrfToken) { throw new Error("Unable to find csrfToken within the nextjs data"); } this.csrfToken = csrfToken; const eligibilityData = { targetType: "user_id", targetId: paymentOptions.targetUserDetails.userId, amountInCents: paymentOptions.amountInCents, action: paymentOptions.type, note: paymentOptions.note }; const eligibilityResponse = yield fetch( `https://account.venmo.com/api/eligibility`, { method: "POST", headers: { cookie: `v_id=${DEVICE_ID}; w_fc=${this.w_fc}; _csrf=${this.csrfCookie}; api_access_token=${this.accessToken};`, "user-agent": USER_AGENT, "csrf-token": this.csrfToken, "xsrf-token": this.csrfToken, "Content-Type": "application/json" }, body: JSON.stringify(eligibilityData) } ); const eligibility = yield eligibilityResponse.json(); const paymentData = __spreadProps(__spreadValues({}, paymentOptions), { eligibilityToken: eligibility.eligibilityToken }); const headers = { cookie: `v_id=${DEVICE_ID}; _csrf=${this.csrfCookie}; api_access_token=${this.accessToken}; w_fc=${this.w_fc};`, "user-agent": USER_AGENT, "csrf-token": this.csrfToken, "xsrf-token": this.csrfToken, "Content-Type": "application/json" }; console.log("POST /api/payments - Data", paymentData); console.log("POST /api/payments - Headers", headers); console.log("csrfToken Second", this.csrfToken); return yield fetch("https://account.venmo.com/api/payments", { method: "POST", headers, body: JSON.stringify(paymentData) }); }); } oldPay(paymentOptions) { return __async(this, null, function* () { if (!this.accessToken || !this.csrfToken || !this.csrfCookie || !this.w_fc) { throw new Error("You are not authenticated. Maybe run login first."); } const headers = { "user-agent": USER_AGENT, "Content-Type": "application/json", "Authorization": `Bearer ${this.accessToken}` }; return yield fetch("https://api.venmo.com/v1/payments", { method: "POST", headers, credentials: "include", body: JSON.stringify(paymentOptions) }); }); } pay(options) { return __async(this, null, function* () { return yield payment(options, this.credentials); }); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Venmo });