@banksnussman/venmo
Version:
Interact with Venmo from Typescript
497 lines (489 loc) • 17.7 kB
JavaScript
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
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 __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
import "dotenv/config";
import { request } from "graphql-request";
import { v4 as v42 } from "uuid";
// src/constants.ts
import { v4 } from "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-${v4()}`;
var GRAPHQL_ENDPOINT = "https://api.venmo.com/graphql";
// src/graphql/funding.ts
import { gql } from "graphql-request";
var FundingInstrumentsQuery = 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
import { gql as gql2 } from "graphql-request";
var PeopleQuery = gql2`
query People($input: SearchInput!) {
search(input: $input) {
people {
edges {
node {
displayName
id
type
avatar {
url
}
handle
firstName
lastName
isFriend
}
}
}
}
}
`;
// src/pay.ts
import { chromium } from "playwright";
function payment(options, credentials) {
return __async(this, null, function* () {
const browser = yield 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 = v42();
}
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 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 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);
});
}
};
export {
Venmo
};