Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Z 330 proposal notifications #277

Merged
merged 12 commits into from
Jul 29, 2024
6 changes: 3 additions & 3 deletions .easignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@

## Unnecessary files
/api
/site
# contracts is required by lib
# packages/* are required
/contracts
/docs
# packages/{chain,lib} are required
140 changes: 70 additions & 70 deletions api/dbschema/edgeql-js/__spec__.ts

Large diffs are not rendered by default.

154 changes: 77 additions & 77 deletions api/dbschema/edgeql-js/modules/default.ts

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions api/src/core/expo/expo.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ export class ExpoService {
constructor(private db: DatabaseService) {}

async sendNotification(messages: (Omit<ExpoPushMessage, 'to'> & { to: ExpoPushToken })[]) {
const responses = (await this.expo.sendPushNotificationsAsync(messages)).map((ticket, i) => ({
ticket,
if (!messages.length) return;

const responses = (await this.expo.sendPushNotificationsAsync(messages)).map((message, i) => ({
ticket: message,
to: messages[i].to,
}));

Expand Down
1 change: 1 addition & 0 deletions api/src/feat/policies/existing-policies.edgeql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ with account := (select Account filter .address = <UAddress>$account),
keys := array_unpack(<array<uint16>>$policyKeys)
select Policy {
key,
name,
approvers: { address },
threshold,
actions: {
Expand Down
2 changes: 2 additions & 0 deletions api/src/feat/policies/existing-policies.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type ExistingPoliciesArgs = {

export type ExistingPoliciesReturns = Array<{
"key": number;
"name": string;
"approvers": Array<{
"address": string;
}>;
Expand Down Expand Up @@ -46,6 +47,7 @@ with account := (select Account filter .address = <UAddress>$account),
keys := array_unpack(<array<uint16>>$policyKeys)
select Policy {
key,
name,
approvers: { address },
threshold,
actions: {
Expand Down
3 changes: 3 additions & 0 deletions api/src/feat/policies/policies.events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ export class PoliciesEventsProcessor {
}

private async markStateAsActive(chain: Chain, log: Log, key: PolicyKey) {
// FIXME: when multiple policies are activated in one block, the wrong one may be marked as active
// This *always* occurs when a policy is activated by a policy update transaction

const account = asUAddress(log.address, chain);
const r = await this.db.exec(activatePolicy, {
account,
Expand Down
2 changes: 1 addition & 1 deletion api/src/feat/policies/policies.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class PoliciesResolver {

@Mutation(() => [Policy])
async proposePolicies(@Input() input: ProposePoliciesInput, @Info() info: GraphQLResolveInfo) {
const policies = await this.service.propose(input);
const policies = await this.service.propose(input, ...input.policies);
return this.service.policies(
policies.map((p) => p.id),
getShape(info),
Expand Down
39 changes: 23 additions & 16 deletions api/src/feat/policies/policies.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import {
validateMessage,
validateTransaction,
Policy,
Address,
UAddress,
PLACEHOLDER_ACCOUNT_ADDRESS,
Tx,
UUID,
asUUID,
Expand All @@ -31,10 +29,9 @@ import { ShapeFunc } from '~/core/database';
import {
policyStateAsPolicy,
PolicyShape,
policyInputAsStateShape,
inputAsPolicyState,
selectPolicy,
latestPolicy2,
inputAsPolicy,
} from './policies.util';
import { NameTaken, PolicyEvent, Policy as PolicyModel, ValidationError } from './policies.model';
import { TX_SHAPE, transactionAsTx, ProposalTxShape } from '../transactions/transactions.util';
Expand Down Expand Up @@ -119,16 +116,25 @@ export class PoliciesService {
const currentPolicies = await this.db.exec(existingPolicies, { account, policyKeys });
const changedPolicies = policiesWithKeys
.map((input) => {
const policy = inputAsPolicy(input.key, input);
const existing = currentPolicies.find((p) => p.key === policy.key);
return (
(!existing || encodePolicy(policy) !== encodePolicy(policyStateAsPolicy(existing))) && {
input,
policy,
}
);
// Merge exisiting policy state (draft or latest) with input
const existing = currentPolicies.find((p) => p.key === input.key);
const policyState = inputAsPolicyState(input.key, input, existing);
const policy = policyStateAsPolicy(policyState);

// Ignore unchanged policies
if (existing && encodePolicy(policy) === encodePolicy(policyStateAsPolicy(existing)))
return;

return {
policy,
state: {
name: 'Policy ' + input.key,
...policyState,
},
};
})
.filter(Boolean);
if (!changedPolicies.length) return [];

// Propose transaction with policy inserts
const transaction = !isInitialization
Expand All @@ -151,15 +157,16 @@ export class PoliciesService {
await this.db.exec(insertPolicies, {
account,
transaction,
policies: changedPolicies.map(({ input }) => ({
policies: changedPolicies.map(({ state }) => ({
...(isInitialization && { activationBlock: 0n }),
...policyInputAsStateShape(input.key, input),
name: input.name || 'Policy ' + input.key,
...state,
})),
})
).map((p) => ({ ...p, id: asUUID(p.id), key: asPolicyKey(p.key) }));

const approvers = new Set(changedPolicies.flatMap(({ input }) => input.approvers));
const approvers = new Set(
changedPolicies.flatMap(({ state }) => state.approvers.map((a) => asAddress(a.address))),
);
this.userAccounts.invalidateApproversCache(...approvers);

newPolicies.forEach(({ id }) =>
Expand Down
4 changes: 2 additions & 2 deletions api/src/feat/policies/policies.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ export const policyStateAsPolicy = <S extends PolicyShape>(state: S) =>
})
: null) as S extends null ? Policy | null : Policy;

export const policyInputAsStateShape = (
export const inputAsPolicyState = (
key: PolicyKey,
p: Partial<PolicyInput>,
defaults: NonNullable<PolicyShape> = {
Expand Down Expand Up @@ -183,7 +183,7 @@ export const policyInputAsStateShape = (
};

export const inputAsPolicy = (key: PolicyKey, p: PolicyInput) =>
policyStateAsPolicy(policyInputAsStateShape(key, p));
policyStateAsPolicy(inputAsPolicyState(key, p));

export const asTransfersConfig = (c: TransfersConfigInput): TransfersConfig => ({
defaultAllow: c.defaultAllow,
Expand Down
13 changes: 13 additions & 0 deletions api/src/feat/proposals/approvers-to-notify.edgeql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
with p := (select Proposal filter .id = <uuid>$proposal),
approvers := p.policy.approvers,
responses := count((select p.<proposal[is ProposalResponse] limit 2)),
shouldNotify := ((responses = 1) if p.proposedBy in approvers else (responses = 0)),
approversToNotify := (approvers if shouldNotify else {})
select {
isTransaction := exists [p is Transaction],
approvers := (
select approversToNotify {
pushToken := .details.pushToken
} filter exists .pushToken
)
};
32 changes: 32 additions & 0 deletions api/src/feat/proposals/approvers-to-notify.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// GENERATED by @edgedb/generate v0.5.3

import type {Executor} from "edgedb";

export type ApproversToNotifyArgs = {
readonly "proposal": string;
};

export type ApproversToNotifyReturns = {
"isTransaction": boolean;
"approvers": Array<{
"pushToken": string | null;
}>;
};

export function approversToNotify(client: Executor, args: ApproversToNotifyArgs): Promise<ApproversToNotifyReturns> {
return client.queryRequiredSingle(`\
with p := (select Proposal filter .id = <uuid>$proposal),
approvers := p.policy.approvers,
responses := count((select p.<proposal[is ProposalResponse] limit 2)),
shouldNotify := ((responses = 1) if p.proposedBy in approvers else (responses = 0)),
approversToNotify := (approvers if shouldNotify else {})
select {
isTransaction := exists [p is Transaction],
approvers := (
select approversToNotify {
pushToken := .details.pushToken
} filter exists .pushToken
)
};`, args);

}
2 changes: 2 additions & 0 deletions api/src/feat/proposals/proposals.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { ProposalsService } from './proposals.service';
import { ProposalsResolver } from './proposals.resolver';
import { ExpoModule } from '~/core/expo/expo.module';

@Module({
imports: [ExpoModule],
providers: [ProposalsService, ProposalsResolver],
exports: [ProposalsService],
})
Expand Down
29 changes: 28 additions & 1 deletion api/src/feat/proposals/proposals.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { rejectProposal } from './reject-proposal.query';
import { approveProposal } from './approve-proposal.query';
import { UserInputError } from '@nestjs/apollo';
import { deleteResponse } from './delete-response.query';
import { approversToNotify } from './approvers-to-notify.query';
import { ExpoService } from '~/core/expo/expo.service';

export type UniqueProposal = UUID;

Expand All @@ -38,6 +40,7 @@ export class ProposalsService {
private db: DatabaseService,
private networks: NetworksService,
private pubsub: PubsubService,
private expo: ExpoService,
) {}

async selectUnique(id: UUID, shape: ShapeFunc<typeof e.Proposal>) {
Expand Down Expand Up @@ -77,7 +80,7 @@ export class ProposalsService {
...shape?.(p),
...(pendingFilter ? { pendingFilter } : {}), // Must be included in the select (not just the filter) to avoid bug
filter: and(e.op(p.account, '=', e.cast(e.Account, account)), pendingFilter),
order_by: p.createdAt,
order_by: { expression: p.createdAt, direction: e.DESC },
};
}),
{ account },
Expand Down Expand Up @@ -110,6 +113,7 @@ export class ProposalsService {
});

this.event(approval.proposal, ProposalEvent.approval);
this.notifyApprovers(proposal);
}

async reject(proposal: UUID) {
Expand Down Expand Up @@ -166,4 +170,27 @@ export class ProposalsService {
event,
});
}

async notifyApprovers(proposal: UUID) {
// Get proposal policy approvers if proposer has approved or can't else {}
const p = await this.db.exec(approversToNotify, { proposal });
if (!p) return;

await this.expo.sendNotification(
p.approvers
.filter((a) => a.pushToken)
.map((a) => ({
to: a.pushToken!,
title: `Approval required for ${p.isTransaction ? `transaction` : `message`}`,
channelId: 'activity',
priority: 'normal',
data: {
href: {
pathname: `/(nav)/transaction/[id]`,
params: { id: proposal },
},
},
})),
);
}
}
3 changes: 2 additions & 1 deletion api/src/feat/transactions/transactions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,13 @@ export class TransactionsService {
});
const id = asUUID(r.id);

this.proposals.event({ id, account }, ProposalEvent.create);
if (signature) {
await this.approve({ id, signature }, true);
} else {
afterRequest(() => this.tryExecute(id));
}
this.proposals.event({ id, account }, ProposalEvent.create);
this.proposals.notifyApprovers(id);

return id;
}
Expand Down
4 changes: 1 addition & 3 deletions api/src/feat/transfers/transfers.events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,7 @@ export class TransfersEvents {
false,
),
})
.unlessConflict((t) => ({
on: e.tuple([t.account, t.block, t.logIndex]),
})),
.unlessConflict(),
(t) => ({
id: true,
internal: true,
Expand Down
19 changes: 18 additions & 1 deletion app/src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,29 @@ import { GoogleAuthProvider } from '#/cloud/google/GoogleAuthProvider';
import { Try } from 'expo-router/build/views/Try';
import { PortalProvider } from '@gorhom/portal';
import { GlobalSubscriptions } from '#/GlobalSubscriptions/GlobalSubscriptions';
import { createStyles, useStyles } from '@theme/styles';

export const unstable_settings = {
initialRouteName: `index`,
};

function Layout() {
const { styles } = useStyles(stylesheet);

return (
<Stack screenOptions={{ headerShown: false }}>
<Stack
screenOptions={{
headerShown: false,
contentStyle: styles.stackContent,
}}
>
<Stack.Screen
name={`(modal)`}
options={{
presentation: 'transparentModal',
animation: 'fade',
animationDuration: 100,
contentStyle: styles.transparentContent,
}}
/>
<Stack.Screen
Expand All @@ -47,12 +56,20 @@ function Layout() {
presentation: 'transparentModal',
animation: 'fade',
animationDuration: 100,
contentStyle: styles.transparentContent,
}}
/>
</Stack>
);
}

const stylesheet = createStyles(({ colors }) => ({
stackContent: {
backgroundColor: colors.surface,
},
transparentContent: {},
}));

function RootLayout() {
return (
<SafeAreaProvider>
Expand Down
5 changes: 0 additions & 5 deletions app/src/app/onboard/_layout.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion app/src/app/scan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default function ScanScreen() {
showError('Failed to connect. Please refresh the DApp and try again');
}
} else if (parseAppLink(data)) {
router.replace(parseAppLink(data)!);
router.push(parseAppLink(data)!);
return true;
}

Expand Down
4 changes: 2 additions & 2 deletions app/src/components/NotificationSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@ export function NotificationSettings({ next }: NotificationSettingsProps) {
))}

<Actions horizontal>
{!perm?.granted && next && <Button onPress={next}>Skip</Button>}

{(!perm?.granted || next) && (
<Button
mode="contained"
Expand All @@ -117,6 +115,8 @@ export function NotificationSettings({ next }: NotificationSettingsProps) {
{perm?.granted ? 'Continue' : 'Enable'}
</Button>
)}

{!perm?.granted && next && <Button onPress={next}>Skip</Button>}
</Actions>
</>
);
Expand Down
Loading
Loading