Skip to content

Commit 043c75e

Browse files
authored
Adds Owner Invoker (#106)
* Adds Owner Invoker * invoker test
1 parent 5c9ae43 commit 043c75e

File tree

5 files changed

+177
-2
lines changed

5 files changed

+177
-2
lines changed

programs/smart-wallet/src/lib.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,36 @@ pub mod smart_wallet {
275275
]];
276276
do_execute_transaction(ctx, wallet_seeds)
277277
}
278+
279+
/// Invokes an arbitrary instruction as a PDA derived from the owner,
280+
/// i.e. as an "Owner Invoker".
281+
///
282+
/// This is useful for using the multisig as a whitelist or as a council,
283+
/// e.g. a whitelist of approved owners.
284+
#[access_control(ctx.accounts.validate())]
285+
pub fn owner_invoke_instruction(
286+
ctx: Context<OwnerInvokeInstruction>,
287+
index: u64,
288+
bump: u8,
289+
ix: TXInstruction,
290+
) -> ProgramResult {
291+
let smart_wallet = &ctx.accounts.smart_wallet;
292+
// Execute the transaction signed by the smart_wallet.
293+
let invoker_seeds: &[&[&[u8]]] = &[&[
294+
b"GokiSmartWalletOwnerInvoker" as &[u8],
295+
&smart_wallet.key().to_bytes(),
296+
&index.to_le_bytes(),
297+
&[bump],
298+
]];
299+
300+
solana_program::program::invoke_signed(
301+
&(&ix).into(),
302+
ctx.remaining_accounts,
303+
invoker_seeds,
304+
)?;
305+
306+
Ok(())
307+
}
278308
}
279309

280310
/// Accounts for [smart_wallet::create_smart_wallet].
@@ -365,6 +395,15 @@ pub struct ExecuteTransaction<'info> {
365395
pub owner: Signer<'info>,
366396
}
367397

398+
/// Accounts for [smart_wallet::owner_invoke_instruction].
399+
#[derive(Accounts)]
400+
pub struct OwnerInvokeInstruction<'info> {
401+
/// The [SmartWallet].
402+
pub smart_wallet: Account<'info, SmartWallet>,
403+
/// An owner of the [SmartWallet].
404+
pub owner: Signer<'info>,
405+
}
406+
368407
fn do_execute_transaction(ctx: Context<ExecuteTransaction>, seeds: &[&[&[u8]]]) -> ProgramResult {
369408
for ix in ctx.accounts.transaction.instructions.iter() {
370409
solana_program::program::invoke_signed(&(ix).into(), ctx.remaining_accounts, seeds)?;

programs/smart-wallet/src/validators.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,10 @@ impl<'info> Validate<'info> for ExecuteTransaction<'info> {
8484
Ok(())
8585
}
8686
}
87+
88+
impl<'info> Validate<'info> for OwnerInvokeInstruction<'info> {
89+
fn validate(&self) -> ProgramResult {
90+
self.smart_wallet.owner_index(self.owner.key())?;
91+
Ok(())
92+
}
93+
}

src/wrappers/smartWallet/index.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import type {
1010
SmartWalletTransactionData,
1111
} from "../../programs";
1212
import type { GokiSDK } from "../../sdk";
13-
import { findTransactionAddress, findWalletDerivedAddress } from "./pda";
13+
import {
14+
findOwnerInvokerAddress,
15+
findTransactionAddress,
16+
findWalletDerivedAddress,
17+
} from "./pda";
1418
import type {
1519
InitSmartWalletWrapperArgs,
1620
NewTransactionArgs,
@@ -173,6 +177,15 @@ export class SmartWalletWrapper {
173177
return await findWalletDerivedAddress(this.key, index);
174178
}
175179

180+
/**
181+
* Finds the owner invoker address and bump of a given index.
182+
* @param index
183+
* @returns
184+
*/
185+
async findOwnerInvokerAddress(index: number): Promise<[PublicKey, number]> {
186+
return await findOwnerInvokerAddress(this.key, index);
187+
}
188+
176189
private async _fetchExecuteTransactionContext({
177190
transactionKey,
178191
owner = this.provider.wallet.publicKey,
@@ -238,6 +251,51 @@ export class SmartWalletWrapper {
238251
return new TransactionEnvelope(this.provider, [ix]);
239252
}
240253

254+
/**
255+
* Executes a transaction using an owner invoker address.
256+
*/
257+
async ownerInvokeInstruction({
258+
instruction,
259+
index,
260+
owner = this.provider.wallet.publicKey,
261+
}: {
262+
instruction: TransactionInstruction;
263+
index: number;
264+
owner?: PublicKey;
265+
}): Promise<TransactionEnvelope> {
266+
const [invokerAddress, invokerBump] = await this.findOwnerInvokerAddress(
267+
index
268+
);
269+
const ix = this.program.instruction.ownerInvokeInstruction(
270+
new BN(index),
271+
invokerBump,
272+
instruction,
273+
{
274+
accounts: {
275+
smartWallet: this.key,
276+
owner,
277+
},
278+
remainingAccounts: [
279+
{
280+
pubkey: instruction.programId,
281+
isSigner: false,
282+
isWritable: false,
283+
},
284+
...instruction.keys.map((k) => {
285+
if (k.isSigner && invokerAddress.equals(k.pubkey)) {
286+
return {
287+
...k,
288+
isSigner: false,
289+
};
290+
}
291+
return k;
292+
}),
293+
],
294+
}
295+
);
296+
return new TransactionEnvelope(this.provider, [ix]);
297+
}
298+
241299
/**
242300
* setOwners
243301
*/

src/wrappers/smartWallet/pda.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,23 @@ export const findWalletDerivedAddress = async (
4646
GOKI_ADDRESSES.SmartWallet
4747
);
4848
};
49+
50+
/**
51+
* Finds an Owner Invoker address of a Smart Wallet.
52+
* @param smartWallet
53+
* @param index
54+
* @returns
55+
*/
56+
export const findOwnerInvokerAddress = async (
57+
smartWallet: PublicKey,
58+
index: number
59+
): Promise<[PublicKey, number]> => {
60+
return await PublicKey.findProgramAddress(
61+
[
62+
utils.bytes.utf8.encode("GokiSmartWalletOwnerInvoker"),
63+
smartWallet.toBuffer(),
64+
new u64(index).toBuffer(),
65+
],
66+
GOKI_ADDRESSES.SmartWallet
67+
);
68+
};

tests/smartWallet.spec.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import "chai-bn";
22

33
import * as anchor from "@project-serum/anchor";
44
import { expectTX } from "@saberhq/chai-solana";
5-
import { TransactionEnvelope } from "@saberhq/solana-contrib";
5+
import {
6+
PendingTransaction,
7+
TransactionEnvelope,
8+
} from "@saberhq/solana-contrib";
69
import { sleep, u64 } from "@saberhq/token-utils";
710
import {
811
Keypair,
@@ -437,4 +440,52 @@ describe("smartWallet", () => {
437440
);
438441
});
439442
});
443+
444+
describe("owner invoker", () => {
445+
const { provider } = sdk;
446+
const ownerA = web3.Keypair.generate();
447+
const ownerB = web3.Keypair.generate();
448+
449+
const owners = [
450+
ownerA.publicKey,
451+
ownerB.publicKey,
452+
provider.wallet.publicKey,
453+
];
454+
let smartWalletWrapper: SmartWalletWrapper;
455+
456+
beforeEach(async () => {
457+
const { smartWalletWrapper: wrapperInner, tx } = await sdk.newSmartWallet(
458+
{
459+
numOwners: owners.length,
460+
owners,
461+
threshold: new BN(1),
462+
}
463+
);
464+
await expectTX(tx, "create new smartWallet").to.be.fulfilled;
465+
smartWalletWrapper = wrapperInner;
466+
});
467+
468+
it("should invoke 1 of N", async () => {
469+
const index = 5;
470+
const [invokerKey] = await smartWalletWrapper.findOwnerInvokerAddress(
471+
index
472+
);
473+
474+
await new PendingTransaction(
475+
provider.connection,
476+
await provider.connection.requestAirdrop(invokerKey, LAMPORTS_PER_SOL)
477+
).wait();
478+
479+
const invokeTX = await smartWalletWrapper.ownerInvokeInstruction({
480+
index,
481+
instruction: SystemProgram.transfer({
482+
fromPubkey: invokerKey,
483+
toPubkey: provider.wallet.publicKey,
484+
lamports: LAMPORTS_PER_SOL,
485+
}),
486+
});
487+
await expectTX(invokeTX, "transfer lamports to smart wallet").to.be
488+
.fulfilled;
489+
});
490+
});
440491
});

0 commit comments

Comments
 (0)