Skip to content

Commit

Permalink
feat: add transaction_id to frame message (#1754)
Browse files Browse the repository at this point in the history
## Motivation

Frame
[transactions](https://www.notion.so/warpcast/Frames-Transactions-Public-Draft-v2-9d9f9f4f527249519a41bd8d16165f73)
require an additional `transaction_id` field in the frame message body.

## Change Summary

- Add `transaction_id` field to `FrameActionBody`.
- Limit to 256 bytes.

## Merge Checklist

_Choose all relevant options below by adding an `x` now or at any time
before submitting for review_

- [x] PR title adheres to the [conventional
commits](https://www.conventionalcommits.org/en/v1.0.0/) standard
- [x] PR has a
[changeset](https://github.com/farcasterxyz/hub-monorepo/blob/main/CONTRIBUTING.md#35-adding-changesets)
- [x] PR has been tagged with a change label(s) (i.e. documentation,
feature, bugfix, or chore)
- [ ] PR includes
[documentation](https://github.com/farcasterxyz/hub-monorepo/blob/main/CONTRIBUTING.md#32-writing-docs)
if necessary.
- [x] All [commits have been
signed](https://github.com/farcasterxyz/hub-monorepo/blob/main/CONTRIBUTING.md#22-signing-commits)

## Additional Context

If this is a relatively large or complex change, provide more details
here that will help reviewers


<!-- start pr-codex -->

---

## PR-Codex overview
This PR adds a transaction ID field to frame messages for chain-specific
actions.

### Detailed summary
- Added `transaction_id` field to frame messages
- Updated message schemas in different packages
- Updated validation tests for various message bodies

> The following files were skipped due to too many changes:
`packages/core/src/validations.test.ts`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your
question}`

<!-- end pr-codex -->
  • Loading branch information
horsefacts committed Feb 29, 2024
1 parent 82204fe commit 579d29a
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 22 deletions.
7 changes: 7 additions & 0 deletions .changeset/cuddly-wolves-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@farcaster/hub-nodejs": patch
"@farcaster/hub-web": patch
"@farcaster/core": patch
---

feat: add transaction ID to frame message
19 changes: 19 additions & 0 deletions packages/core/src/protobufs/generated/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,8 @@ export interface FrameActionBody {
inputText: Uint8Array;
/** Serialized frame state value */
state: Uint8Array;
/** Chain-specific transaction ID for tx actions */
transactionId: Uint8Array;
}

function createBaseMessage(): Message {
Expand Down Expand Up @@ -1775,6 +1777,7 @@ function createBaseFrameActionBody(): FrameActionBody {
castId: undefined,
inputText: new Uint8Array(),
state: new Uint8Array(),
transactionId: new Uint8Array(),
};
}

Expand All @@ -1795,6 +1798,9 @@ export const FrameActionBody = {
if (message.state.length !== 0) {
writer.uint32(42).bytes(message.state);
}
if (message.transactionId.length !== 0) {
writer.uint32(50).bytes(message.transactionId);
}
return writer;
},

Expand Down Expand Up @@ -1840,6 +1846,13 @@ export const FrameActionBody = {

message.state = reader.bytes();
continue;
case 6:
if (tag != 50) {
break;
}

message.transactionId = reader.bytes();
continue;
}
if ((tag & 7) == 4 || tag == 0) {
break;
Expand All @@ -1856,6 +1869,7 @@ export const FrameActionBody = {
castId: isSet(object.castId) ? CastId.fromJSON(object.castId) : undefined,
inputText: isSet(object.inputText) ? bytesFromBase64(object.inputText) : new Uint8Array(),
state: isSet(object.state) ? bytesFromBase64(object.state) : new Uint8Array(),
transactionId: isSet(object.transactionId) ? bytesFromBase64(object.transactionId) : new Uint8Array(),
};
},

Expand All @@ -1869,6 +1883,10 @@ export const FrameActionBody = {
(obj.inputText = base64FromBytes(message.inputText !== undefined ? message.inputText : new Uint8Array()));
message.state !== undefined &&
(obj.state = base64FromBytes(message.state !== undefined ? message.state : new Uint8Array()));
message.transactionId !== undefined &&
(obj.transactionId = base64FromBytes(
message.transactionId !== undefined ? message.transactionId : new Uint8Array(),
));
return obj;
},

Expand All @@ -1885,6 +1903,7 @@ export const FrameActionBody = {
: undefined;
message.inputText = object.inputText ?? new Uint8Array();
message.state = object.state ?? new Uint8Array();
message.transactionId = object.transactionId ?? new Uint8Array();
return message;
},
};
Expand Down
118 changes: 96 additions & 22 deletions packages/core/src/validations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,11 @@ describe("validateCastAddBody", () => {
});

test("when text is empty", () => {
const body = Factories.CastAddBody.build({ text: "", mentions: [], mentionsPositions: [] });
const body = Factories.CastAddBody.build({
text: "",
mentions: [],
mentionsPositions: [],
});
expect(validations.validateCastAddBody(body)).toEqual(ok(body));
});

Expand Down Expand Up @@ -318,12 +322,18 @@ describe("validateCastAddBody", () => {
});

test("with an embed url string over 256 ASCII characters", () => {
body = Factories.CastAddBody.build({ embeds: [], embedsDeprecated: [faker.random.alphaNumeric(257)] });
body = Factories.CastAddBody.build({
embeds: [],
embedsDeprecated: [faker.random.alphaNumeric(257)],
});
hubErrorMessage = "url > 256 bytes";
});

test("with an embed url string over 256 bytes", () => {
body = Factories.CastAddBody.build({ embeds: [], embedsDeprecated: [`${faker.random.alphaNumeric(254)}🤓`] });
body = Factories.CastAddBody.build({
embeds: [],
embedsDeprecated: [`${faker.random.alphaNumeric(254)}🤓`],
});
hubErrorMessage = "url > 256 bytes";
});

Expand Down Expand Up @@ -356,12 +366,16 @@ describe("validateCastAddBody", () => {
});

test("when text is null", () => {
body = Factories.CastAddBody.build({ text: null as unknown as undefined });
body = Factories.CastAddBody.build({
text: null as unknown as undefined,
});
hubErrorMessage = "text is missing";
});

test("when text is longer than 320 ASCII characters", () => {
body = Factories.CastAddBody.build({ text: faker.random.alphaNumeric(321) });
body = Factories.CastAddBody.build({
text: faker.random.alphaNumeric(321),
});
hubErrorMessage = "text > 320 bytes";
});

Expand Down Expand Up @@ -423,7 +437,9 @@ describe("validateCastAddBody", () => {
});

test("when parent hash is missing", () => {
body = Factories.CastAddBody.build({ parentCastId: Factories.CastId.build({ hash: undefined }) });
body = Factories.CastAddBody.build({
parentCastId: Factories.CastId.build({ hash: undefined }),
});
hubErrorMessage = "hash is missing";
});

Expand All @@ -436,7 +452,10 @@ describe("validateCastAddBody", () => {
});

test("with a parentUrl of empty string", () => {
body = Factories.CastAddBody.build({ parentUrl: "", parentCastId: undefined });
body = Factories.CastAddBody.build({
parentUrl: "",
parentCastId: undefined,
});
hubErrorMessage = "url < 1 byte";
});

Expand Down Expand Up @@ -523,7 +542,10 @@ describe("validateCastAddBody", () => {
});

test("with non-integer mentionsPositions", () => {
body = Factories.CastAddBody.build({ mentions: [Factories.Fid.build()], mentionsPositions: [1.5] });
body = Factories.CastAddBody.build({
mentions: [Factories.Fid.build()],
mentionsPositions: [1.5],
});
hubErrorMessage = "mentionsPositions must be integers";
});

Expand Down Expand Up @@ -568,12 +590,17 @@ describe("validateReactionBody", () => {
});

test("with invalid reaction type", () => {
body = Factories.ReactionBody.build({ type: 100 as unknown as protobufs.ReactionType });
body = Factories.ReactionBody.build({
type: 100 as unknown as protobufs.ReactionType,
});
hubErrorMessage = "invalid reaction type";
});

test("without target", () => {
body = Factories.ReactionBody.build({ targetCastId: undefined, targetUrl: undefined });
body = Factories.ReactionBody.build({
targetCastId: undefined,
targetUrl: undefined,
});
hubErrorMessage = "target is missing";
});

Expand All @@ -600,7 +627,10 @@ describe("validateReactionBody", () => {
});

test("with a targetUrl of empty string", () => {
body = Factories.ReactionBody.build({ targetUrl: "", targetCastId: undefined });
body = Factories.ReactionBody.build({
targetUrl: "",
targetCastId: undefined,
});
hubErrorMessage = "url < 1 byte";
});

Expand Down Expand Up @@ -734,7 +764,9 @@ describe("validateVerificationAddEthAddressBody", () => {
});

test("with missing block hash", async () => {
body = Factories.VerificationAddAddressBody.build({ blockHash: undefined });
body = Factories.VerificationAddAddressBody.build({
blockHash: undefined,
});
hubErrorMessage = "blockHash is missing";
});

Expand Down Expand Up @@ -833,7 +865,14 @@ describe("validateVerificationAddEthAddressSignature", () => {
chainId,
verificationType: 1,
},
{ transient: { fid, network, contractSignature: true, protocol: Protocol.ETHEREUM } },
{
transient: {
fid,
network,
contractSignature: true,
protocol: Protocol.ETHEREUM,
},
},
);
const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network, publicClients);
expect(result.isOk()).toBeTruthy();
Expand All @@ -852,7 +891,14 @@ describe("validateVerificationAddEthAddressSignature", () => {
chainId,
verificationType: 1,
},
{ transient: { fid, network, contractSignature: true, protocol: Protocol.ETHEREUM } },
{
transient: {
fid,
network,
contractSignature: true,
protocol: Protocol.ETHEREUM,
},
},
);
const result = await validations.validateVerificationAddEthAddressSignature(body, fid, network, publicClients);
expect(result).toEqual(err(new HubError("bad_request.validation_failure", "invalid claimSignature")));
Expand Down Expand Up @@ -884,12 +930,16 @@ describe("validateVerificationAddEthAddressSignature", () => {

describe("validateVerificationRemoveBody", () => {
test("ethereum-succeeds", () => {
const body = Factories.VerificationRemoveBody.build({ protocol: Protocol.ETHEREUM });
const body = Factories.VerificationRemoveBody.build({
protocol: Protocol.ETHEREUM,
});
expect(validations.validateVerificationRemoveBody(body)).toEqual(ok(body));
});

test("solana-succeeds", () => {
const body = Factories.VerificationRemoveBody.build({ protocol: Protocol.SOLANA });
const body = Factories.VerificationRemoveBody.build({
protocol: Protocol.SOLANA,
});
expect(validations.validateVerificationRemoveBody(body)).toEqual(ok(body));
});

Expand Down Expand Up @@ -936,7 +986,10 @@ describe("validateUserDataAddBody", () => {
});

test("succeeds for ens names", async () => {
const body = Factories.UserDataBody.build({ type: UserDataType.USERNAME, value: "averylongensname.eth" });
const body = Factories.UserDataBody.build({
type: UserDataType.USERNAME,
value: "averylongensname.eth",
});
expect(validations.validateUserDataAddBody(body)).toEqual(ok(body));
});

Expand Down Expand Up @@ -998,7 +1051,9 @@ describe("validateUsernameProof", () => {
});
test("when name does not end with .eth", async () => {
const proof = await Factories.UsernameProofMessage.create({
data: { usernameProofBody: { name: utf8StringToBytes("aname")._unsafeUnwrap() } },
data: {
usernameProofBody: { name: utf8StringToBytes("aname")._unsafeUnwrap() },
},
});
const result = await validations.validateUsernameProofBody(proof.data.usernameProofBody, proof.data);
const hubError = result._unsafeUnwrapErr();
Expand Down Expand Up @@ -1147,7 +1202,10 @@ describe("validateMessageData", () => {
test("fails with embedsDeprecated when timestamp is past cut-off", async () => {
const data = Factories.CastAddData.build({
timestamp: validations.EMBEDS_V1_CUTOFF + 1,
castAddBody: Factories.CastAddBody.build({ embeds: [], embedsDeprecated: [faker.internet.url()] }),
castAddBody: Factories.CastAddBody.build({
embeds: [],
embedsDeprecated: [faker.internet.url()],
}),
});
const result = await validations.validateMessageData(data);
expect(result).toEqual(err(new HubError("bad_request.validation_failure", "string embeds have been deprecated")));
Expand All @@ -1156,7 +1214,10 @@ describe("validateMessageData", () => {
test("fails with embedsDeprecated when timestamp is at cut-off", async () => {
const data = Factories.CastAddData.build({
timestamp: validations.EMBEDS_V1_CUTOFF,
castAddBody: Factories.CastAddBody.build({ embeds: [], embedsDeprecated: [faker.internet.url()] }),
castAddBody: Factories.CastAddBody.build({
embeds: [],
embedsDeprecated: [faker.internet.url()],
}),
});
const result = await validations.validateMessageData(data);
expect(result).toEqual(err(new HubError("bad_request.validation_failure", "string embeds have been deprecated")));
Expand All @@ -1165,7 +1226,10 @@ describe("validateMessageData", () => {
test("succeeds with embedsDeprecated when timestamp is before cut-off", async () => {
const data = Factories.CastAddData.build({
timestamp: validations.EMBEDS_V1_CUTOFF - 1,
castAddBody: Factories.CastAddBody.build({ embeds: [], embedsDeprecated: [faker.internet.url()] }),
castAddBody: Factories.CastAddBody.build({
embeds: [],
embedsDeprecated: [faker.internet.url()],
}),
});
const result = await validations.validateMessageData(data);
expect(result).toEqual(ok(data));
Expand All @@ -1178,7 +1242,10 @@ describe("validateFrameActionBody", () => {
const body = Factories.FrameActionBody.build({
buttonIndex: 1,
url: Buffer.from("https://example.com"),
castId: { fid: Factories.Fid.build(), hash: Factories.MessageHash.build() },
castId: {
fid: Factories.Fid.build(),
hash: Factories.MessageHash.build(),
},
});
const result = validations.validateFrameActionBody(body);
expect(result.isOk()).toBeTruthy();
Expand Down Expand Up @@ -1212,4 +1279,11 @@ describe("validateFrameActionBody", () => {
const result = validations.validateFrameActionBody(body);
expect(result._unsafeUnwrapErr().message).toMatch("invalid state");
});
test("fails when transaction ID is too long", async () => {
const body = Factories.FrameActionBody.build({
transactionId: Buffer.from(faker.datatype.string(257)),
});
const result = validations.validateFrameActionBody(body);
expect(result._unsafeUnwrapErr().message).toMatch("invalid transaction ID");
});
});
3 changes: 3 additions & 0 deletions packages/core/src/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,9 @@ export const validateFrameActionBody = (body: protobufs.FrameActionBody): HubRes
if (validateBytesAsString(body.state, 4096).isErr()) {
return err(new HubError("bad_request.validation_failure", "invalid state"));
}
if (validateBytesAsString(body.transactionId, 256).isErr()) {
return err(new HubError("bad_request.validation_failure", "invalid transaction ID"));
}

if (body.castId !== undefined) {
const result = validateCastId(body.castId);
Expand Down
Loading

0 comments on commit 579d29a

Please sign in to comment.