Skip to content

Commit

Permalink
Templates, 0.3.1-beta
Browse files Browse the repository at this point in the history
  • Loading branch information
jacksondoherty committed Oct 23, 2024
1 parent f71ba0c commit dbbc4b6
Show file tree
Hide file tree
Showing 20 changed files with 463 additions and 244 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# TipLink Open Source

This repository contains a collection of open-source code maintained by TipLink Corp. It provides tools and examples for integrating TipLink functionality into third-party websites and applications. The code is organized into separate folders, each with its own specific purpose and licensing.

## Repository Structure

- `/api`: Libraries for third-party websites to create TipLinks and access TipLink Pro features.
- `/mailer`: An example [Next.js](https://nextjs.org/) app demonstrating how to create TipLinks, Escrow TipLinks, and email them.

---

TipLink Corp © [2024]
4 changes: 2 additions & 2 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tiplink/api",
"version": "0.2.6",
"version": "0.3.1",
"description": "Api for creating and sending TipLinks",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
160 changes: 159 additions & 1 deletion api/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,20 @@ interface AnalyticsSummary {
campaign_info: Record<string, number>[];
}

// TODO better typing with prisma across repo or should we just not include all event types?
enum EventType {
CREATED="CREATED",
ACCESSED="ACCESSED",
CLAIMED="CLAIMED",
CLAIMED_BACK="CLAIMED_BACK",
}

interface Analytic {
public_key: PublicKey;
event: EventType;
created_at: Date;
}

enum Rate {
DAILY = 0,
WEEKLY,
Expand Down Expand Up @@ -545,6 +559,45 @@ export class Campaign extends TipLinkApi {
return entries;
}

public async getAnalytic(publicKey: string): Promise<Analytic | null> {
const analyticsRes = await this.client.fetch(
`campaigns/${this.id}/analytics`,
{ public_key: publicKey, },
);

let analytic: Analytic | null = null;
analyticsRes.forEach((res: Record<string, any>) => {
// TODO should we display most recent created_at in category?
if (res.event == "CLAIMED" || res.event == "TRANSFERED" || res.event == "RECREATED" || res.event == "WITHDRAWN") {
analytic = {
public_key: new PublicKey(res.public_key),
event: EventType.CLAIMED,
created_at: new Date(res.created_at),
};
} else if (res.event == "CLAIMED_BACK" && (!analytic || analytic.event !== EventType.CLAIMED)) {
analytic = {
public_key: new PublicKey(res.public_key),
event: EventType.CLAIMED_BACK,
created_at: new Date(res.created_at),
};
} else if (res.event == "ACCESSED" && (!analytic || (analytic.event !== EventType.CLAIMED && analytic.event !== EventType.CLAIMED_BACK))) {
analytic = {
public_key: new PublicKey(res.public_key),
event: EventType.ACCESSED,
created_at: new Date(res.created_at),
};
} else if (res.event == "CREATED" && (!analytic || (analytic.event !== EventType.CLAIMED && analytic.event !== EventType.CLAIMED_BACK && analytic.event !== EventType.ACCESSED))) {
analytic = {
public_key: new PublicKey(res.public_key),
event: EventType.CREATED,
created_at: new Date(res.created_at),
};
}
});

return analytic;
}

public async getAnalytics(): Promise<AnalyticsSummary> {
// TODO clean up response here and type
const analyticsRes = await this.client.fetch(
Expand Down Expand Up @@ -983,11 +1036,33 @@ interface MintCreateParams extends Themeable {
feeTransactionHash?: string;
}

interface MintFindParams {
campaign_id: string;
}

interface FeeResponse {
publicKey: PublicKey;
feeLamports: number;
}

interface IndividualLinkGetMintActivityParam {
urlSlug: string;
destination: never;
}

interface NonIndividualLinkGetMintActivityParam {
destination: string;
urlSlug: never;
}

type GetMintActivityParam = IndividualLinkGetMintActivityParam; // | NonIndividualLinkMintActivityParam;

interface MintActivity {
claim_txn: string;
timestamp: Date;
destination: PublicKey;
}

class MintActions extends TipLinkApi {
public constructor(params: MintActionsConstructor) {
super(params.client);
Expand Down Expand Up @@ -1234,6 +1309,61 @@ class MintActions extends TipLinkApi {

return mint;
}

public async find(params: MintFindParams): Promise<Mint> {
const res = (
(await this.client.fetch(
`campaigns/${params.campaign_id}/mint/specs`,
{ campaign_id: params.campaign_id},
null,
"GET",
)) as Record<string, any> // TODO type this
);

const mintParams: MintConstructorParams = {
client: this.client,
id: Number(res["id"]),
campaign_id: res["campaign_id"],
mintName: res["name"],
symbol: res["symbol"],
mintDescription: res["description"],
campaignName: res['name'],
collectionId: res["collection_mint"],
treeAddress: res["tree_address"],
jsonUri: res["json_uri"],
creatorPublicKey: new PublicKey(res["creator"]),
attributes: res["attributes"],
mintLimit: Number(res["mint_limit"]),
collectionUri: res["collection_uri"],
imageUrl: res["image"],
dataHost: res["imageHost"],
externalUrl: res["external_url"],
royalties: res["seller_fee_basis_points"],
primaryUrlSlug: res["primary_url_slug"],
rotatingUrlSlug: res["rotating_url_slug"],
useRotating: res["use_rotating"],
// rotating_seed_key: res["rotating_seed_key"],
rotatingTimeInterval: Number(res["rotating_time_interval"]),
totpWindow: res["totp_window"],
userClaimLimit: res["user_claim_limit"],
};

if (
Object.prototype.hasOwnProperty.call(
res,
"royalties_destination",
) &&
typeof res["royalties_destination"] === "string"
) {
mintParams["royaltiesDestination"] = new PublicKey(
res["royalties_destination"],
);
}

const mint = new Mint(mintParams);

return mint;
}
}

export class Mint extends TipLinkApi {
Expand Down Expand Up @@ -1310,11 +1440,39 @@ export class Mint extends TipLinkApi {
public async getAnalytics(): Promise<AnalyticsSummary> {
// TODO clean up response here and type
const analyticsRes = await this.client.fetch(
`campaigns/${this.campaign_id}/analytics_summary`,
`campaigns/${this.campaign_id}/mint/analytics/summary`,
);
return analyticsRes;
}

public async getMintActivity(params: GetMintActivityParam): Promise<MintActivity | null> {
// TODO should this only work for non individual links?

const activitiesRes = await this.client.fetch(
`campaigns/${this.campaign_id}/mint/activities`,
{ url_slug: params.urlSlug },
);

if (Object.prototype.hasOwnProperty.call(activitiesRes, 'activities') && activitiesRes.activities.length > 0) {
const activity = activitiesRes.activities[0];
// @ts-ignore
if (Object.prototype.hasOwnProperty.call(activity, 'mint')) {
return {
claim_txn: activity.mint.claim_txn,
timestamp: new Date(activity.mint.timestamp),
destination: new PublicKey(activity.mint.destination),
};
}
return {
claim_txn: activity.claim_txn,
timestamp: new Date(activity.timestamp),
destination: new PublicKey(activity.destination),
};
}

return null;
}

public async share(email: string, admin = false): Promise<boolean> {
const accounts = await this.client.fetch(`accounts_public`, {
torus_id: email,
Expand Down
2 changes: 1 addition & 1 deletion api/src/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const DEFAULT_VERIFY_EMAIL_ENDPOINT =
"https://email-verify.tiplink.tech/verify-email";
export const VERIFY_EMAIL_ENDPOINT =
process !== undefined && process.env !== undefined
? process.env.NEXT_PUBLIC_VERIFY_EMAIL_ENDPOINT_OVERRIDE ??
? process.env.NEXT_PUBLIC_VERIFY_EMAIL_ENDPOINT_OVERRIDE ||
DEFAULT_VERIFY_EMAIL_ENDPOINT
: DEFAULT_VERIFY_EMAIL_ENDPOINT;

Expand Down
20 changes: 14 additions & 6 deletions api/src/enclave.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { PublicKey } from "@solana/web3.js";

import { EscrowTipLink, TipLink } from ".";
import { isEmailValid } from "./email";
import { BACKEND_URL_BASE } from "./escrow/constants";
import { INDEXER_URL_BASE } from "./escrow/constants";

const DEFAULT_ENCLAVE_ENDPOINT = "https://mailer.tiplink.io";
const ENCLAVE_ENDPOINT =
process !== undefined && process.env !== undefined
? process.env.NEXT_PUBLIC_ENCLAVE_ENDPOINT_OVERRIDE ??
? process.env.NEXT_PUBLIC_ENCLAVE_ENDPOINT_OVERRIDE ||
DEFAULT_ENCLAVE_ENDPOINT
: DEFAULT_ENCLAVE_ENDPOINT;

Expand Down Expand Up @@ -67,7 +67,7 @@ export async function getReceiverEmail(
): Promise<string> {
// We actually no longer hit the enclave here but we'll keep in this file
// since the enclave manages this data.
const endpoint = `${BACKEND_URL_BASE}/api/v1/generated-tiplinks/${publicKey.toString()}/email`;
const endpoint = `${INDEXER_URL_BASE}/api/v1/generated-tiplinks/${publicKey.toString()}/email`;

const res = await fetch(endpoint, {
method: "GET",
Expand Down Expand Up @@ -103,6 +103,7 @@ export async function getReceiverEmail(
* @param toName - Optional name of the recipient for the email.
* @param replyEmail - Optional email address for the recipient to reply to.
* @param replyName - Optional name of the sender for the email.
* @param templateName - Optional name of the template to be used for the email.
* @returns A promise that resolves when the email has been sent.
* @throws Throws an error if the HTTP request fails with a non-ok status.
*/
Expand All @@ -112,7 +113,8 @@ export async function mail(
toEmail: string,
toName?: string,
replyEmail?: string,
replyName?: string
replyName?: string,
templateName?: string
): Promise<void> {
if (!(await isEmailValid(toEmail))) {
throw new Error("Invalid email address");
Expand All @@ -126,6 +128,7 @@ export async function mail(
replyEmail,
replyName,
tiplinkUrl: tipLink.url.toString(),
templateName,
};
const res = await fetch(url, {
method: "POST",
Expand All @@ -147,6 +150,7 @@ export async function mail(
* @param toName - Optional name of the recipient for the email.
* @param replyEmail - Optional email address for the recipient to reply to.
* @param replyName - Optional name of the sender for the email.
* @param templateName - Optional name of the template to be used for the email.
* @returns A promise that resolves when the email has been sent.
* @throws Throws an error if the HTTP request fails with a non-ok status.
*/
Expand All @@ -156,6 +160,7 @@ interface MailEscrowWithObjArgs {
toName?: string;
replyEmail?: string;
replyName?: string;
templateName?: string;
}

/**
Expand All @@ -166,6 +171,7 @@ interface MailEscrowWithObjArgs {
* @param toName - Optional name of the recipient for the email.
* @param replyEmail - Optional email address for the recipient to reply to.
* @param replyName - Optional name of the sender for the email.
* @param templateName - Optional name of the template to be used for the email.
* @returns A promise that resolves when the email has been sent.
* @throws Throws an error if the HTTP request fails with a non-ok status.
*/
Expand All @@ -177,6 +183,7 @@ interface MailEscrowWithValsArgs {
toName?: string;
replyEmail?: string;
replyName?: string;
templateName?: string;
}

/**
Expand All @@ -187,7 +194,7 @@ export async function mailEscrow(
): Promise<void> {
// TODO: Require API key / ensure deposited

const { apiKey, toName, replyEmail, replyName } = args;
const { apiKey, toName, replyEmail, replyName, templateName } = args;
const { escrowTipLink } = args as MailEscrowWithObjArgs;
let { toEmail, pda, receiverTipLink } = args as MailEscrowWithValsArgs;

Expand All @@ -210,7 +217,7 @@ export async function mailEscrow(

const receiverUrlOverride =
process !== undefined && process.env !== undefined
? process.env.NEXT_PUBLIC_ESCROW_RECEIVER_URL_OVERRIDE
? process.env.NEXT_PUBLIC_ESCROW_RECEIVER_URL_OVERRIDE || undefined
: undefined;

const body = {
Expand All @@ -221,6 +228,7 @@ export async function mailEscrow(
pda: pda.toString(),
tiplinkPublicKey: receiverTipLink.toString(),
receiverUrlOverride,
templateName,
};
const res = await fetch(url, {
method: "POST",
Expand Down
10 changes: 5 additions & 5 deletions api/src/escrow/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ const DEFAULT_DEPOSIT_URL_BASE =
"https://tiplink-mailer.vercel.app/depositor-url";
export const DEPOSIT_URL_BASE =
process !== undefined && process.env !== undefined
? process.env.NEXT_PUBLIC_ESCROW_DEPOSITOR_URL_OVERRIDE ??
? process.env.NEXT_PUBLIC_ESCROW_DEPOSITOR_URL_OVERRIDE ||
DEFAULT_DEPOSIT_URL_BASE
: DEFAULT_DEPOSIT_URL_BASE;

export const PRIO_FEES_LAMPORTS = 10_000;

const DEFAULT_BACKEND_URL_BASE = "https://backend.tiplink.io";
export const BACKEND_URL_BASE =
const DEFAULT_INDEXER_URL_BASE = "https://backend.tiplink.io";
export const INDEXER_URL_BASE =
process !== undefined && process.env !== undefined
? process.env.NEXT_PUBLIC_BACKEND_URL_OVERRIDE ?? DEFAULT_BACKEND_URL_BASE
: DEFAULT_BACKEND_URL_BASE;
? process.env.NEXT_PUBLIC_INDEXER_URL_OVERRIDE || DEFAULT_INDEXER_URL_BASE
: DEFAULT_INDEXER_URL_BASE;
Loading

0 comments on commit dbbc4b6

Please sign in to comment.