Skip to content

Commit

Permalink
feat: farcaster client transaction attribution (#474)
Browse files Browse the repository at this point in the history
  • Loading branch information
stephancill authored Aug 15, 2024
1 parent 3eeb57b commit 52d7028
Show file tree
Hide file tree
Showing 10 changed files with 64 additions and 11 deletions.
6 changes: 6 additions & 0 deletions .changeset/tender-foxes-admire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@frames.js/debugger": patch
"@frames.js/render": patch
---

feat: farcaster client transaction attribution
6 changes: 6 additions & 0 deletions docs/pages/reference/render/use-frame.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ A function to handle redirect responses from the frame. This happens when you cl

A function to handle transaction button presses, returns the transaction hash or null. The function is called when a user presses a transaction button and endpoint specified by `target` returns transaction data.

### `transactionDataSuffix`

- Type: `0x${string}`

A suffix to add to the transaction data. Useful for [client attribution](https://www.notion.so/warpcast/Frame-Transactions-Public-9d9f9f4f527249519a41bd8d16165f73#c1c3182208ce4ae4a7ffa72129b9795a) in farcaster. Use the `attribution` helper from `@frames.js/render/farcaster` to generate this.

### `frameContext`

- Type: `FrameContext`
Expand Down
24 changes: 14 additions & 10 deletions packages/debugger/app/debugger-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type FrameActionBodyPayload,
type OnConnectWalletFunc,
} from "@frames.js/render";
import { attribution } from "@frames.js/render/farcaster";
import { useFrame } from "@frames.js/render/use-frame";
import { ConnectButton, useConnectModal } from "@rainbow-me/rainbowkit";
import { sendTransaction, signTypedData, switchChain } from "@wagmi/core";
Expand Down Expand Up @@ -573,23 +574,26 @@ export default function DebuggerPage({
const farcasterFrameConfig: UseFrameOptions<
FarcasterSigner | null,
FrameActionBodyPayload
> = useMemo(
() => ({
> = useMemo(() => {
const attributionData = process.env.NEXT_PUBLIC_FARCASTER_ATTRIBUTION_FID
? attribution(parseInt(process.env.NEXT_PUBLIC_FARCASTER_ATTRIBUTION_FID))
: undefined;
return {
...useFrameConfig,
signerState: farcasterSignerState,
specification: "farcaster",
frameContext: {
...farcasterFrameContext.frameContext,
address: account.address || farcasterFrameContext.frameContext.address,
},
}),
[
account.address,
farcasterFrameContext.frameContext,
farcasterSignerState,
useFrameConfig,
]
);
transactionDataSuffix: attributionData,
};
}, [
account.address,
farcasterFrameContext.frameContext,
farcasterSignerState,
useFrameConfig,
]);

const useFrameHook = useMemo(() => {
return () => {
Expand Down
4 changes: 4 additions & 0 deletions packages/debugger/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ declare global {
* Used in combination with FARCASTER_DEVELOPER_MNEMONIC if SIGNER_URL is not provided.
*/
FARCASTER_DEVELOPER_FID: string | undefined;
/**
* FID to be attributed for farcaster frame transactions.
*/
NEXT_PUBLIC_FARCASTER_ATTRIBUTION_FID: string | undefined;
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/render/src/farcaster/attribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { encodeAbiParameters } from "viem";

export function attribution(fid: number): `0x${string}` {
return encodeAbiParameters(
[{ type: "bytes1" }, { type: "uint32" }],
["0xfc", fid]
);
}
1 change: 1 addition & 0 deletions packages/render/src/farcaster/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./frames";
export * from "./signers";
export * from "./attribution";
4 changes: 4 additions & 0 deletions packages/render/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import * as farcasterAll from "./farcaster";

export { fallbackFrameContext } from "./fallback-frame-context";

const { attribution: _, ...farcaster } = farcasterAll;
export { farcaster };
export * from "./farcaster";
export * from "./frame-ui";
export * from "./collapsed-frame-ui";
Expand Down
4 changes: 4 additions & 0 deletions packages/render/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ export type UseFetchFrameOptions<
* Called after transaction data has been returned from the server and user needs to approve the transaction.
*/
onTransaction: OnTransactionFunc;
/** Transaction data suffix */
transactionDataSuffix?: `0x${string}`;
onSignature: OnSignatureFunc;
onComposerFormAction: OnComposerFormActionFunc;
/**
Expand Down Expand Up @@ -169,6 +171,8 @@ export type UseFrameOptions<
onMint?: (t: OnMintArgs) => void;
/** a function to handle transaction buttons that returned transaction data from the target, returns the transaction hash or null */
onTransaction?: OnTransactionFunc;
/** Transaction data suffix */
transactionDataSuffix?: `0x${string}`;
/** A function to handle transaction buttons that returned signature data from the target, returns signature hash or null */
onSignature?: OnSignatureFunc;
/**
Expand Down
16 changes: 15 additions & 1 deletion packages/render/src/use-fetch-frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import type {
ComposerActionStateFromMessage,
ErrorMessageResponse,
} from "frames.js/types";
import { hexToBytes } from "viem";
import type { FarcasterFrameContext } from "./farcaster";
import type {
CastActionRequest,
ComposerActionRequest,
Expand All @@ -28,7 +30,6 @@ import type {
UseFetchFrameOptions,
UseFetchFrameSignFrameActionFunction,
} from "./types";
import type { FarcasterFrameContext } from "./farcaster";
import { isParseResult } from "./use-frame-stack";

class UnexpectedCastActionResponseError extends Error {
Expand Down Expand Up @@ -104,6 +105,7 @@ export function useFetchFrame<
extraButtonRequestPayload,
signFrameAction,
onTransaction,
transactionDataSuffix,
onSignature,
onError = defaultErrorHandler,
fetchFn,
Expand Down Expand Up @@ -521,6 +523,18 @@ export function useFetchFrame<

// get transaction id or signature id from transaction data
if (transactionData.method === "eth_sendTransaction") {
// Add transaction data suffix
if (
transactionData.params.data &&
transactionData.attribution !== false &&
transactionDataSuffix &&
// Has a function signature
hexToBytes(transactionData.params.data).length > 4
) {
transactionData.params.data = (transactionData.params.data +
transactionDataSuffix.slice(2)) as `0x${string}`;
}

transactionIdOrError = await tryCall(
onTransaction({
frame: sourceFrame,
Expand Down
2 changes: 2 additions & 0 deletions packages/render/src/use-frame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export function useFrame<
dangerousSkipSigning,
onMint = onMintFallback,
onTransaction = onTransactionFallback,
transactionDataSuffix,
onConnectWallet = onConnectWalletFallback,
onSignature = onSignatureFallback,
connectedAddress,
Expand Down Expand Up @@ -171,6 +172,7 @@ export function useFrame<
frameActionProxy,
frameGetProxy,
onTransaction,
transactionDataSuffix,
onSignature,
signFrameAction({ actionContext, forceRealSigner }) {
return dangerousSkipSigning && !forceRealSigner
Expand Down

0 comments on commit 52d7028

Please sign in to comment.