Skip to content

Commit

Permalink
Merge pull request #277 from zallo-labs/Z-330-proposal-notifications
Browse files Browse the repository at this point in the history
Z 330 proposal notifications
  • Loading branch information
hbriese authored Jul 29, 2024
2 parents 9c9bee2 + 7aa96e2 commit bd03dca
Show file tree
Hide file tree
Showing 25 changed files with 301 additions and 195 deletions.
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

0 comments on commit bd03dca

Please sign in to comment.