Skip to content

Commit

Permalink
process transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
kevtechi committed Oct 27, 2024
1 parent 5a827f0 commit ba20e1e
Show file tree
Hide file tree
Showing 10 changed files with 247 additions and 12 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
CHAIN_ID=100
IPFS_URL=https://ipfs.internal.citizenwallet.xyz
DEFAULT_PROFILE_IMAGE_IPFS_HASH=ipfs://Qm...

# Supabase
SUPABASE_URL=https://...
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"imports": {
"@/firebase": "./supabase/functions/_firebase",
"@/supabase": "./supabase",
"@citizenwallet/sdk": "jsr:@citizenwallet/sdk@^2.0.6",
"@citizenwallet/sdk": "jsr:@citizenwallet/sdk@^2.0.7",
"@supabase/functions-js": "jsr:@supabase/functions-js@^2.4.3",
"@supabase/supabase-js": "jsr:@supabase/supabase-js@^2.45.6",
"ethers": "npm:ethers@^6.13.4",
Expand Down
8 changes: 4 additions & 4 deletions deno.lock

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

32 changes: 30 additions & 2 deletions supabase/functions/_citizen-wallet/contracts/profiles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import profileAbi from "./Profile.abi.json" with {
type: "json",
};
import { downloadJsonFromIpfs } from "../../ipfs/index.ts";
import type { ProfileWithTokenId } from "../../../_db/profiles.ts";

export const getProfileFromId = async (
config: CommunityConfig,
id: string,
): Promise<Profile | undefined> => {
): Promise<ProfileWithTokenId | undefined> => {
const rpc = new JsonRpcProvider(config.primaryRPCUrl);

const contract = new Contract(
Expand All @@ -35,7 +36,34 @@ export const getProfileFromId = async (
throw new Error("IPFS_URL is not set");
}

return formatProfileImageLinks(baseUrl, profile);
return {
...formatProfileImageLinks(baseUrl, profile),
token_id: id,
};
} catch (error) {
console.error("Error fetching profile:", error);
return;
}
};

export const getProfileFromAddress = async (
config: CommunityConfig,
address: string,
): Promise<ProfileWithTokenId | undefined> => {
const rpc = new JsonRpcProvider(config.primaryRPCUrl);

const contract = new Contract(
config.community.profile.address,
profileAbi,
rpc,
);

try {
const id: bigint = await contract.getFunction("fromAddressToId")(
address,
);

return getProfileFromId(config, id.toString());
} catch (error) {
console.error("Error fetching profile:", error);
return;
Expand Down
12 changes: 12 additions & 0 deletions supabase/functions/_citizen-wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export interface ERC20TransferData {
value: string;
}

export interface ERC20TransferExtraData {
description: string;
}

export interface MetadataUpdateData {
_tokenId: string;
}
Expand All @@ -24,6 +28,14 @@ export const communityConfig = () => {
return new CommunityConfig(communityJson);
};

export const formatERC20TransactionValue = (
config: CommunityConfig,
value: string,
) => {
const token = config.primaryToken;
return formatUnits(value, token.decimals);
};

export const createERC20TransferNotification = (
config: CommunityConfig,
data: ERC20TransferData,
Expand Down
37 changes: 37 additions & 0 deletions supabase/functions/_citizen-wallet/profiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { SupabaseClient } from "jsr:@supabase/supabase-js@2";
import type { CommunityConfig } from "jsr:@citizenwallet/sdk";
import {
getProfile,
insertAnonymousProfile,
upsertProfile,
} from "../_db/profiles.ts";
import { getProfileFromAddress } from "./contracts/profiles/index.ts";

export const ensureProfileExists = async (
client: SupabaseClient,
config: CommunityConfig,
address: string,
) => {
const { data, error } = await getProfile(
client,
address,
);

if (error || !data) {
// Check the smart contract for a profile
const profile = await getProfileFromAddress(
config,
address,
);

if (profile) {
await upsertProfile(client, profile);
} else {
// There is none, let's create an anonymous profile in the database
await insertAnonymousProfile(
client,
address,
);
}
}
};
39 changes: 35 additions & 4 deletions supabase/functions/_db/profiles.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,50 @@
import type { Profile } from "jsr:@citizenwallet/sdk";
import { formatProfileImageLinks, type Profile } from "jsr:@citizenwallet/sdk";
import type {
PostgrestSingleResponse,
SupabaseClient,
} from "jsr:@supabase/supabase-js@2";

export interface ProfileWithTokenId extends Profile {
token_id: string;
}

const PROFILES_TABLE = "a_profiles";

export const insertAnonymousProfile = async (
client: SupabaseClient,
account: string,
): Promise<PostgrestSingleResponse<null>> => {
const defaultProfileImageIpfsHash = Deno.env.get(
"DEFAULT_PROFILE_IMAGE_IPFS_HASH",
);
if (!defaultProfileImageIpfsHash) {
throw new Error("DEFAULT_PROFILE_IMAGE_IPFS_HASH is not set");
}

const ipfsUrl = Deno.env.get("IPFS_URL");
if (!ipfsUrl) {
throw new Error("IPFS_URL is not set");
}

const profile: Profile = formatProfileImageLinks(ipfsUrl, {
account,
username: "@anonymous",
name: "Anonymous",
description: "This user does not have a profile",
image: defaultProfileImageIpfsHash,
image_medium: defaultProfileImageIpfsHash,
image_small: defaultProfileImageIpfsHash,
});
return client.from(PROFILES_TABLE).insert(profile);
};

export const upsertProfile = async (
client: SupabaseClient,
profile: Profile,
tokenId: string,
profile: ProfileWithTokenId,
): Promise<PostgrestSingleResponse<null>> => {
return client
.from(PROFILES_TABLE)
.upsert({ ...profile, token_id: tokenId }, {
.upsert(profile, {
onConflict: "account",
});
};
Expand Down
25 changes: 25 additions & 0 deletions supabase/functions/_db/transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import type { SupabaseClient } from "jsr:@supabase/supabase-js@2";
import type { TransactionStatus } from "jsr:@citizenwallet/sdk";

export interface Transaction {
id: string;
hash: string;
created_at: string;
updated_at: string;
from: string;
to: string;
value: string;
description: string;
status: TransactionStatus;
}

const TRANSACTIONS_TABLE = "a_transactions";

export const upsertTransaction = async (
client: SupabaseClient,
transaction: Transaction,
) => {
return client.from(TRANSACTIONS_TABLE).upsert(transaction, {
onConflict: "id",
});
};
1 change: 0 additions & 1 deletion supabase/functions/download-profile-data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ Deno.serve(async (req) => {
const result = await upsertProfile(
supabaseClient,
profile,
metadataUpdateData._tokenId,
);

console.log(result);
Expand Down
102 changes: 102 additions & 0 deletions supabase/functions/process-tx/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Follow this setup guide to integrate the Deno language server with your editor:
// https://deno.land/manual/getting_started/setup_your_environment
// This enables autocomplete, go to definition, etc.

// Setup type definitions for built-in Supabase Runtime APIs
import "jsr:@supabase/functions-js/edge-runtime.d.ts";

import {
communityConfig,
type ERC20TransferData,
type ERC20TransferExtraData,
formatERC20TransactionValue,
} from "../_citizen-wallet/index.ts";
import { getServiceRoleClient } from "../_db/index.ts";
import { type Transaction, upsertTransaction } from "../_db/transactions.ts";
import { ensureProfileExists } from "../_citizen-wallet/profiles.ts";

Deno.serve(async (req) => {
const { record } = await req.json();

console.log("record", record);

if (!record || typeof record !== "object") {
return new Response("Invalid record data", { status: 400 });
}

const {
hash,
tx_hash,
created_at,
updated_at,
dest,
status,
data,
extra_data,
} = record;

if (!dest || typeof dest !== "string") {
return new Response(
"Destination address is required and must be a string",
{ status: 400 },
);
}

const community = communityConfig();

if (dest.toLowerCase() !== community.primaryToken.address.toLowerCase()) {
return new Response("Only process primary token transfers", {
status: 200,
});
}

// Initialize Supabase client
const supabaseClient = getServiceRoleClient();

const chainId = Deno.env.get("CHAIN_ID");
if (!chainId) {
return new Response("CHAIN_ID is required", { status: 500 });
}

const erc20TransferData = data as ERC20TransferData;

await ensureProfileExists(supabaseClient, community, erc20TransferData.from);
await ensureProfileExists(supabaseClient, community, erc20TransferData.to);

let erc20TransferExtraData: ERC20TransferExtraData = { description: "" };
if (extra_data) {
erc20TransferExtraData = extra_data as ERC20TransferExtraData;
}

// insert transaction into db
const transaction: Transaction = {
id: hash,
hash: tx_hash,
created_at,
updated_at,
from: erc20TransferData.from,
to: erc20TransferData.to,
value: formatERC20TransactionValue(community, erc20TransferData.value),
description: erc20TransferExtraData.description || "",
status: status,
};

const { error } = await upsertTransaction(supabaseClient, transaction);
if (error) {
console.error("Error inserting transaction:", error);
}

return new Response("notification sent", { status: 200 });
});

/* To invoke locally:
1. Run `supabase start` (see: https://supabase.com/docs/reference/cli/supabase-start)
2. Make an HTTP request:
curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/notify-successful-transaction' \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{"name":"Functions"}'
*/

0 comments on commit ba20e1e

Please sign in to comment.