Skip to content

Commit

Permalink
invites: referral bonus, anti-sybil, invite graph tracking (#744)
Browse files Browse the repository at this point in the history
* invites: referral bonus, anti-sybil, invite graph tracking

* review comments

* copy changes
  • Loading branch information
nalinbhardwaj authored Feb 24, 2024
1 parent c4c5de0 commit 4386409
Show file tree
Hide file tree
Showing 29 changed files with 557 additions and 251 deletions.
26 changes: 19 additions & 7 deletions apps/daimo-mobile/src/action/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Hex } from "viem";

import { ActStatus, SetActStatus, useActStatus } from "./actStatus";
import { createEnclaveKey, loadEnclaveKey } from "../logic/enclave";
import { defaultEnclaveKeyName } from "../model/account";
import { defaultEnclaveKeyName, deviceAPIKeyName } from "../model/account";

function getKeySecurityMessage(hwSecLevel: ExpoEnclave.HardwareSecurityLevel) {
switch (hwSecLevel) {
Expand Down Expand Up @@ -57,26 +57,38 @@ export type DeviceKeyStatus = {
message: string;
};

export function useLoadOrCreateEnclaveKey(): DeviceKeyStatus {
const enclaveKeyName = defaultEnclaveKeyName;

function useLoadOrCreateEnclaveKey(keyName: string): DeviceKeyStatus {
const [pubKeyHex, setPubKeyHex] = useState<Hex>();
const [keyStatus, setKeyStatus] = useActStatus("useLoadOrCreateEnclaveKey");

// Load or create enclave key immediately, in the idle state
useEffect(() => {
loadKey(setKeyStatus, enclaveKeyName).then((loadedKeyInfo) => {
loadKey(setKeyStatus, keyName).then((loadedKeyInfo) => {
console.log(`[ACTION] loaded key info ${JSON.stringify(loadedKeyInfo)}`);
if (loadedKeyInfo && !loadedKeyInfo.pubKeyHex) {
createKey(setKeyStatus, enclaveKeyName, loadedKeyInfo.hwSecLevel).then(
createKey(setKeyStatus, keyName, loadedKeyInfo.hwSecLevel).then(
(newPublicKey) => {
console.log(`[ACTION] created public key ${newPublicKey}`);
setPubKeyHex(newPublicKey);
}
);
} else setPubKeyHex(loadedKeyInfo?.pubKeyHex);
});
}, [enclaveKeyName]);
}, [keyName]);

return { pubKeyHex, ...keyStatus };
}

// Primary account enclave key
export function useEnclaveKey(): DeviceKeyStatus {
return useLoadOrCreateEnclaveKey(defaultEnclaveKeyName);
}

// Device API key: Poor man's device attestation. True device attestation is
// often not available on some devices, so we use a poor man's version of it
// -- a key created on device keychain/keystore once and never deleted. Most
// devices make it quite hard to delete a key, so it can serve as a poor man's
// sybil protection.
export function useDeviceAPIKey(): DeviceKeyStatus {
return useLoadOrCreateEnclaveKey(deviceAPIKeyName);
}
16 changes: 14 additions & 2 deletions apps/daimo-mobile/src/action/useCreateAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export function useCreateAccount(
name: string,
inviteLink: DaimoLink | undefined,
daimoChain: DaimoChain,
keyStatus: DeviceKeyStatus
keyStatus: DeviceKeyStatus,
deviceAPIKeyStatus: DeviceKeyStatus
): ActHandle {
const [as, setAS] = useActStatus("useCreateAccount");

Expand All @@ -36,12 +37,23 @@ export function useCreateAccount(
// On exec, create contract onchain, claiming name.
const result = rpcHook.deployWallet.useMutation();
const exec = async () => {
if (!keyStatus.pubKeyHex) return;
if (
!keyStatus.pubKeyHex ||
!deviceAPIKeyStatus.pubKeyHex ||
!sanitisedInviteLink
) {
console.log(
`[CREATE] missing data for useCreateAccount ${keyStatus} ${deviceAPIKeyStatus} ${sanitisedInviteLink}`
);
setAS("error", "Missing data");
return;
}
setAS("loading", "Creating account...");
result.mutate({
name,
pubKeyHex: keyStatus.pubKeyHex,
inviteLink: sanitisedInviteLink,
deviceAttestationString: deviceAPIKeyStatus.pubKeyHex,
});
};

Expand Down
8 changes: 8 additions & 0 deletions apps/daimo-mobile/src/model/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ import { env } from "../logic/env";
export const defaultEnclaveKeyName =
process.env.DAIMO_APP_VARIANT === "dev" ? "daimo-dev-12" : "daimo-12";

/**
* Device API key name: serves as poor man's device attestation.
* Fixed key created once and never deleted -- used as an alternate to
* device attestations.
*/
export const deviceAPIKeyName =
process.env.DAIMO_APP_VARIANT === "dev" ? "daimo-apikey-dev" : "daimo-apikey";

/** Account data stored on device. */
export type Account = {
/** Local device signing key name */
Expand Down
2 changes: 1 addition & 1 deletion apps/daimo-mobile/src/view/TabNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ function SendTab() {
<SendStack.Screen name="SendTransfer" component={SendTransferScreen} />
<SendStack.Screen name="QR" component={QRScreen} />
<SendStack.Screen name="SendLink" component={SendNoteScreen} />
<HomeStack.Screen name="Account" component={AccountScreen} />
<SendStack.Screen name="Account" component={AccountScreen} />
</SendStack.Group>
</SendStack.Navigator>
);
Expand Down
44 changes: 38 additions & 6 deletions apps/daimo-mobile/src/view/screen/AccountScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
EAccount,
canSendTo,
getAccountName,
getAddressContraction,
timeMonth,
} from "@daimo/common";
import { daimoChainFromId } from "@daimo/contract";
Expand All @@ -26,6 +27,7 @@ import { SwipeUpDownRef } from "../shared/SwipeUpDown";
import { ErrorBanner } from "../shared/error";
import {
ParamListHome,
navToAccountPage,
useDisableTabSwipe,
useExitBack,
useExitToHome,
Expand Down Expand Up @@ -59,7 +61,11 @@ function AccountScreenInner(props: Props & { account: Account }) {
<AccountScreenLoader account={props.account} link={params.link} />
)}
{"eAcc" in params && (
<AccountScreenBody account={props.account} eAcc={params.eAcc} />
<AccountScreenBody
account={props.account}
eAcc={params.eAcc}
inviterEAcc={params.inviterEAcc}
/>
)}
</View>
);
Expand All @@ -83,7 +89,7 @@ function AccountScreenLoader({
console.log(`[ACCOUNT] loaded account: ${JSON.stringify(status.data)}`);
nav.navigate("HomeTab", {
screen: "Account",
params: { eAcc: status.data.account },
params: { eAcc: status.data.account, inviterEAcc: status.data.inviter },
});
}, [status.data]);

Expand All @@ -104,9 +110,11 @@ function AccountScreenLoader({
function AccountScreenBody({
account,
eAcc,
inviterEAcc,
}: {
account: Account;
eAcc: EAccount;
inviterEAcc?: EAccount;
}) {
const nav = useNav();
useDisableTabSwipe(nav);
Expand Down Expand Up @@ -148,10 +156,34 @@ function AccountScreenBody({
bottomSheetRef,
});

const onInviterPress = useCallback(() => {
if (!inviterEAcc) return;
navToAccountPage(inviterEAcc, nav);
}, [inviterEAcc, nav]);

// TODO: show other accounts coin+chain, once we support multiple.
const subtitle = eAcc.timestamp
? `Joined ${timeMonth(eAcc.timestamp)}`
: getAccountName({ addr: eAcc.addr });
const subtitle = (() => {
if (inviterEAcc)
return (
<TextBody color={color.gray3}>
Invited by{" "}
<TextBody color={color.midnight} onPress={onInviterPress}>
{getAccountName(inviterEAcc)}
</TextBody>
</TextBody>
);
else if (eAcc.timestamp)
return (
<TextBody color={color.gray3}>
Joined {timeMonth(eAcc.timestamp)}
</TextBody>
);
return (
<TextBody color={color.gray3}>
{getAddressContraction(eAcc.addr)}
</TextBody>
);
})();

// Show linked accounts
const fcAccount = (eAcc.linkedAccounts || [])[0];
Expand All @@ -164,7 +196,7 @@ function AccountScreenBody({
<Spacer h={16} />
<AccountCopyLinkButton eAcc={eAcc} size="h2" center />
<Spacer h={4} />
<TextBody color={color.gray3}>{subtitle}</TextBody>
{subtitle}
{fcAccount && (
<>
<FarcasterButton fcAccount={fcAccount} />
Expand Down
14 changes: 11 additions & 3 deletions apps/daimo-mobile/src/view/screen/onboarding/OnboardingScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { InvitePage } from "./InvitePage";
import { OnboardingHeader } from "./OnboardingHeader";
import { UseExistingPage } from "./UseExistingPage";
import { ActStatus } from "../../../action/actStatus";
import { useLoadOrCreateEnclaveKey } from "../../../action/key";
import { useEnclaveKey, useDeviceAPIKey } from "../../../action/key";
import { useCreateAccount } from "../../../action/useCreateAccount";
import { useExistingAccount } from "../../../action/useExistingAccount";
import { getInitialURLOrTag } from "../../../logic/deeplink";
Expand Down Expand Up @@ -83,15 +83,23 @@ export default function OnboardingScreen({
return () => subscription.remove();
}, []);

const keyStatus = useLoadOrCreateEnclaveKey();
const keyStatus = useEnclaveKey();

const deviceAPIKeyStatus = useDeviceAPIKey();

// Create an account as soon as possible, hiding latency
const {
exec: createExec,
reset: createReset,
status: createStatus,
message: createMessage,
} = useCreateAccount(name, inviteLink, daimoChain, keyStatus);
} = useCreateAccount(
name,
inviteLink,
daimoChain,
keyStatus,
deviceAPIKeyStatus
);

// Use existing account spin loops and waits for the device key to show up
// in any on-chain account.
Expand Down
10 changes: 4 additions & 6 deletions apps/daimo-mobile/src/view/screen/send/RecipientDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { ButtonCircle } from "../../shared/ButtonCircle";
import { ContactBubble } from "../../shared/ContactBubble";
import { FarcasterButton } from "../../shared/FarcasterBubble";
import Spacer from "../../shared/Spacer";
import { useNav } from "../../shared/nav";
import { navToAccountPage, useNav } from "../../shared/nav";
import { TextH3, TextLight } from "../../shared/text";

export function RecipientDisplay({
Expand Down Expand Up @@ -39,11 +39,9 @@ export function RecipientDisplay({

const nav = useNav();
const goToAccount = useCallback(() => {
if (isAccount)
nav.navigate("SendTab", {
screen: "Account",
params: { eAcc: recipient },
});
if (isAccount) {
navToAccountPage(recipient, nav);
}
}, [nav, recipient]);

return (
Expand Down
7 changes: 2 additions & 5 deletions apps/daimo-mobile/src/view/screen/send/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Bubble, ContactBubble } from "../../shared/ContactBubble";
import { LinkedAccountBubble } from "../../shared/LinkedAccountBubble";
import Spacer from "../../shared/Spacer";
import { ErrorRowCentered } from "../../shared/error";
import { useNav } from "../../shared/nav";
import { navToAccountPage, useNav } from "../../shared/nav";
import { color, touchHighlightUnderlay } from "../../shared/style";
import { TextBody, TextCenter, TextLight } from "../../shared/text";
import { useWithAccount } from "../../shared/withAccount";
Expand Down Expand Up @@ -154,10 +154,7 @@ function RecipientRow({
}
case "eAcc": {
if (mode === "account") {
nav.navigate("HomeTab", {
screen: "Account",
params: { eAcc: recipient },
});
navToAccountPage(recipient, nav);
} else {
nav.navigate("SendTab", {
screen: "SendTransfer",
Expand Down
15 changes: 12 additions & 3 deletions apps/daimo-mobile/src/view/shared/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
DisplayOpEvent,
EAccount,
parseDaimoLink,
getEAccountStr,
} from "@daimo/common";
import { NavigatorScreenParams, useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
Expand All @@ -23,7 +24,9 @@ export type QRScreenOptions = "PAY ME" | "SCAN";
export type ParamListHome = {
Home: undefined;
QR: { option: QRScreenOptions | undefined };
Account: { eAcc: EAccount } | { link: DaimoLinkAccount };
Account:
| { eAcc: EAccount; inviterEAcc: EAccount | undefined }
| { link: DaimoLinkAccount };
HistoryOp: { op: DisplayOpEvent };
};

Expand Down Expand Up @@ -54,7 +57,9 @@ export type ParamListSend = {
SendTransfer: SendNavProp;
QR: { option: QRScreenOptions | undefined };
SendLink: { recipient?: MsgContact; lagAutoFocus: boolean };
Account: { eAcc: EAccount };
Account:
| { eAcc: EAccount; inviterEAcc: EAccount | undefined }
| { link: DaimoLinkAccount };
HistoryOp: { op: DisplayOpEvent };
};

Expand Down Expand Up @@ -228,5 +233,9 @@ export function navToAccountPage(account: EAccount, nav: MainNav) {
// currentTab is eg "SendNav", is NOT in fact a ParamListTab:
const currentTab = nav.getState().routes[0].name;
const newTab = currentTab.startsWith("Send") ? "SendTab" : "HomeTab";
nav.navigate(newTab, { screen: "Account", params: { eAcc: account } });
const accountLink = {
type: "account",
account: getEAccountStr(account),
} as DaimoLinkAccount;
nav.navigate(newTab, { screen: "Account", params: { link: accountLink } });
}
Loading

0 comments on commit 4386409

Please sign in to comment.