Why is the public key in `.near-credentials` keypairs seemingly irrelevant? - nearprotocol

Let's say I create an account foo.near on NEAR with a single full access public key ed25519:publiccc which is matched to a private key ed25519:secrettt and stored locally in the .near-credentials/mainnet/foo.near.json file.
I can now send myself 1 NEAR using near send foo.near foo.near 1 to verify the keys work. If I modify the private key in that keypair at all, the send fails. Good.
Then I replace the public key in that keypair with a nonsense string like ed25519:giggle.
I can still send myself 1 NEAR using the same command. Why?
It appears the public key is totally irrelevant in this keypair. You can use a nonsense string or a public key that's supposed to have limited access only and the private key gives full access to transfer funds.

Good question! I had to do some digging. So let's start with what the CLI is doing behind the scenes. The code is found here:
exports.sendMoney = async function (options) {
await checkCredentials(options.sender, options.networkId, options.keyStore);
console.log(`Sending ${options.amount} NEAR to ${options.receiver} from ${options.sender}`);
const near = await connect(options);
const account = await near.account(options.sender);
const result = await account.sendMoney(options.receiver, utils.format.parseNearAmount(options.amount));
inspectResponse.prettyPrintResponse(result, options);
};
Essentially it connects to the network, creates an account object, and then calls the sendMoney method for that account object which is through near-api-js. The way it's instantiated is through near.account() which behind the scenes does the following (found here):
async account(accountId: string): Promise<Account> {
const account = new Account(this.connection, accountId);
return account;
}
This is just calling the class' constructor passing in the connection and account ID. Up until this point, we have no reference to the private or public key other than the connection which has a keystore indicating WHERE the keypair will be found. Ok moving on.
After the account class is initialized, the sendMoney method is called and the only action is a simple transfer with the amount. The code for this method can be seen here.
async sendMoney(receiverId: string, amount: BN): Promise<FinalExecutionOutcome> {
return this.signAndSendTransaction({
receiverId,
actions: [transfer(amount)]
});
}
You'll notice that it calls the signAndSendTransaction method that is part of the Account Class (found here).
async signAndSendTransaction({ receiverId, actions, returnError }: SignAndSendTransactionOptions): Promise<FinalExecutionOutcome> {
let txHash, signedTx;
// TODO: TX_NONCE (different constants for different uses of exponentialBackoff?)
const result = await exponentialBackoff(TX_NONCE_RETRY_WAIT, TX_NONCE_RETRY_NUMBER, TX_NONCE_RETRY_WAIT_BACKOFF, async () => {
[txHash, signedTx] = await this.signTransaction(receiverId, actions);
const publicKey = signedTx.transaction.publicKey;
try {
return await this.connection.provider.sendTransaction(signedTx);
}
...
...
Out of this method, the important thing is this.signTransaction whose method is found here:
protected async signTransaction(receiverId: string, actions: Action[]): Promise<[Uint8Array, SignedTransaction]> {
const accessKeyInfo = await this.findAccessKey(receiverId, actions);
if (!accessKeyInfo) {
throw new TypedError(`Can not sign transactions for account ${this.accountId} on network ${this.connection.networkId}, no matching key pair exists for this account`, 'KeyNotFound');
}
const { accessKey } = accessKeyInfo;
const block = await this.connection.provider.block({ finality: 'final' });
const blockHash = block.header.hash;
const nonce = accessKey.nonce.add(new BN(1));
return await signTransaction(
receiverId, nonce, actions, baseDecode(blockHash), this.connection.signer, this.accountId, this.connection.networkId
);
}
In this method, there's a sneaky this.findAccessKey being done whose code can be found here.
async findAccessKey(receiverId: string, actions: Action[]): Promise<{ publicKey: PublicKey; accessKey: AccessKeyView }> {
// TODO: Find matching access key based on transaction (i.e. receiverId and actions)
const publicKey = await this.connection.signer.getPublicKey(this.accountId, this.connection.networkId);
if (!publicKey) {
throw new TypedError(`no matching key pair found in ${this.connection.signer}`, 'PublicKeyNotFound');
}
const cachedAccessKey = this.accessKeyByPublicKeyCache[publicKey.toString()];
if (cachedAccessKey !== undefined) {
return { publicKey, accessKey: cachedAccessKey };
}
try {
const rawAccessKey = await this.connection.provider.query<AccessKeyViewRaw>({
request_type: 'view_access_key',
account_id: this.accountId,
public_key: publicKey.toString(),
finality: 'optimistic'
});
// store nonce as BN to preserve precision on big number
const accessKey = {
...rawAccessKey,
nonce: new BN(rawAccessKey.nonce),
};
// this function can be called multiple times and retrieve the same access key
// this checks to see if the access key was already retrieved and cached while
// the above network call was in flight. To keep nonce values in line, we return
// the cached access key.
if (this.accessKeyByPublicKeyCache[publicKey.toString()]) {
return { publicKey, accessKey: this.accessKeyByPublicKeyCache[publicKey.toString()] };
}
this.accessKeyByPublicKeyCache[publicKey.toString()] = accessKey;
return { publicKey, accessKey };
} catch (e) {
if (e.type == 'AccessKeyDoesNotExist') {
return null;
}
throw e;
}
}
If you take a look at this code, you'll notice a really important line:
const publicKey = await this.connection.signer.getPublicKey(this.accountId, this.connection.networkId);
What is this returning? The short answer is that if you're using the NEAR-CLI, the connection.signer comes from UnencryptedFileSystemKeyStore since your keys are stored in .near-credentials. Now let's look at this getPublicKey method found here:
async getPublicKey(accountId?: string, networkId?: string): Promise<PublicKey> {
const keyPair = await this.keyStore.getKey(networkId, accountId);
if (keyPair === null) {
return null;
}
return keyPair.getPublicKey();
}
It does this.keyStore.getKey where the keyStore is the UnencryptedFileSystemKeyStore I mentioned earlier. The getKey method is then found here:
async getKey(networkId: string, accountId: string): Promise<KeyPair> {
// Find key / account id.
if (!await exists(this.getKeyFilePath(networkId, accountId))) {
return null;
}
const accountKeyPair = await readKeyFile(this.getKeyFilePath(networkId, accountId));
return accountKeyPair[1];
}
In this function, the final culprit is the readKeyFile where it returns the key pair (both public and private). This can be found here:
export async function readKeyFile(filename: string): Promise<[string, KeyPair]> {
const accountInfo = await loadJsonFile(filename);
// The private key might be in private_key or secret_key field.
let privateKey = accountInfo.private_key;
if (!privateKey && accountInfo.secret_key) {
privateKey = accountInfo.secret_key;
}
return [accountInfo.account_id, KeyPair.fromString(privateKey)];
}
Notice that it returns a KeyPair that is generated from the private key. This means that no matter what the public key is, it will overwrite it. I did a quick test where I console logged the accountInfo and it indeed returned the incorrect public key that I had set manually but when it generates the KeyPair from the private key, that information is overwritten.
TLDR:
When the your account signs a transaction, the public key is re-generated based on the private key. It doesn't matter what public key you have in your .near-credentials since it is overwritten when the KeyPair is created based on the private key.

Related

near-api-js signed contract deploy

I am having trouble getting serverside near-api-js account creation and contract deployment working successfully and would appreciate some input.
I am using a unencrypted local keystore set up as follows;
const privatekey = "__private key__";
const keyPair = KeyPair.fromString(privatekey); // generate key pair
const keyStore = new nearAPI.keyStores.UnencryptedFileSystemKeyStore(credentialsPath); // declare keystore type
const config = Object.assign(require('./config')('testnet'), {
networkId: networkId,
deps: { keyStore },
}); // setup config for near connection with keystore added
// update keystore with main account
await keyStore.setKey('testnet', config.masterAccount, nearAPI.utils.KeyPair.fromString(privatekey));
// connect near with config details
const near = await nearAPI.connect(config);
// load master account
const account = await new nearAPI.Account(near.connection, config.masterAccount);
I am creating a subaccount as;
const amountInYocto = utils.format.parseNearAmount("5");
// generate new public key
const newPublicKey = await near.connection.signer.createKey(newAccountName,config.networkId);
let keyPair;
let publicKey;
keyPair = await KeyPair.fromRandom('ed25519');
publicKey = keyPair.getPublicKey();
await near.connection.signer.keyStore.setKey(near.connection.networkId, newAccountName, keyPair);
const response = await near.createAccount(newAccountName, publicKey, amountInYocto);
When I try to deploy a contract to the created account I get the error;
Actor 'master account' doesn't have permission to account 'subaccount' to complete the action
This is how I deploy a contract:
const txs = [transactions.deployContract(fs.readFileSync(`../near/out/out.wasm`))];
const result = await account.signAndSendTransaction({
receiverId: subaccount,
actions: txs
});
Im sure I have something wrong in how i manage the keys, but cannot work it out. Any suggestions?

How does request.context in Parse Cloud Code work?

Back in Parse Server 3.0 update, there was an addition of request.context to pass data between BeforeSave and AfterSave as documented here:
https://docs.parseplatform.org/cloudcode/guide/#using-request-context
However, I'm having a bit of trouble understanding how and when Parse runs this code in the example.
const beforeSave = function beforeSave(request) {
const { object: role } = request;
// Get users that will be added to the users relation.
const usersOp = role.op('users');
if (usersOp && usersOp.relationsToAdd.length > 0) {
// add the users being added to the request context
request.context = { buyers: usersOp.relationsToAdd };
}
};
const afterSave = function afterSave(request) {
const { object: role, context } = request;
if (context && context.buyers) {
const purchasedItem = getItemFromRole(role);
const promises = context.buyers.map(emailBuyer.bind(null, purchasedItem));
item.increment('orderCount', context.buyers.length);
promises.push(item.save(null, { useMasterKey: true }));
Promise.all(promises).catch(request.log.error.bind(request.log));
}
};
in other examples, cloud code functions are run via Parse.Cloud.beforeSave or Parse.Cloud.afterSave. In this example above, the function beforeSave is assigned to a
const beforeSave.
Why was this done and is this supposed to be placed inside main.js top level or inside another function?

How do I generate private keys for ThunderCore from a 12-word mnemonic seed phrase exported from TrustWallet or Metamask?

I got a 12-word mnemonic seed phrase exported from TrustWallet, however, I need it as a thunderCore private key. How do I generate the thunderCore private key from it? What if the seed phrase is export from Metamask?
To generate private keys from 12-word mnemonic phrases, you need a derivation path (a string) as specified in BIP-0049.
Derivation paths used in the field:
m/44'/1001'/0'/0: uses the correct coin type for ThunderCore (1001) as registered in SLIP-0044, used by TrustWallet
"m/44'/60'/0'/0: is the derivation path used for Ethereum mainnet, used by MetaMask
Here's a self-contained example to generate private keys from the 12-word mnemonic using the ethereum-hdwallet library:
hdwallet.js
const EthereumHDWallet = require('ethereum-hdwallet')
// https://github.com/bitcoin/bips/blob/master/bip-0049.mediawiki
// https://github.com/satoshilabs/slips/blob/master/slip-0044.md
const TrustWalletHdPath = "m/44'/1001'/0'/0" // coin_type 1001 is ThunderCore, this is the recommended path
const MetaMaskHdPath = "m/44'/60'/0'/0" // coin_type 60 is really Ethereum, but MetaMask use it for all EVM compatible chains
class HdWallet {
constructor(mnemonic) {
this.mnemonic = this.mnemonic
this.w = EthereumHDWallet.fromMnemonic(mnemonic)
}
deriveUsingTrustWalletPath() {
return this.w.derive(TrustWalletHdPath)
}
deriveUsingMetaMaskPath() {
return this.w.derive(MetaMaskHdPath)
}
metaMaskAddress(index /*: number */) /*: string */ {
return '0x' + this.deriveUsingMetaMaskPath().derive(index).getAddress().toString('hex')
}
trustWalletAddress(index /*: number */) /*: string */ {
return '0x' + this.deriveUsingTrustWalletPath().derive(index).getAddress().toString('hex')
}
metaMaskPrivateKey(index /*: number */) /*: string */ {
return this.deriveUsingMetaMaskPath().derive(index).getPrivateKey().toString('hex')
}
trustWalletPrivateKey(index /*: number */) /*: string */ {
return this.deriveUsingTrustWalletPath().derive(index).getPrivateKey().toString('hex')
}
}
const fromMnemonic = (s /*: string or buffer */) => {
return new HdWallet(s)
}
module.exports = {
fromMnemonic: fromMnemonic,
}
testHdWallet.js
const assert = require('assert');
const HdWallet = require('../src/hdwallet');
const mnemonic = 'feel pulp crunch segment buzz turn organ broccoli elder ask phone limit';
describe('fromMnemonic', () => {
it('trustWalletAddress', async() => {
const w = HdWallet.fromMnemonic(mnemonic);
console.log('TrustWallet:', w.trustWalletAddress(0));
assert('0x2323Beb990514446bA4c073C2e1A4BDC0ECf06Af'.toLowerCase() ===
w.trustWalletAddress(0).toLowerCase());
});
it('metaMaskAddress', async() => {
const w = HdWallet.fromMnemonic(mnemonic);
console.log('MetaMask:', w.metaMaskAddress(0));
assert('0x9A7be7ae9a2779167bc5b64d1cC672cc5b2593e4'.toLowerCase() ===
w.metaMaskAddress(0).toLowerCase());
});
it('trustWalletPrivateKey', async() => {
const w = HdWallet.fromMnemonic(mnemonic);
console.log('TrustWallet sk:', w.trustWalletPrivateKey(0));
assert('6d7bf444545ce47d7fda9df58275f5f4dd5eb911494ab66d81f76f1aca2b763e'.toLowerCase() ===
w.trustWalletPrivateKey(0).toLowerCase());
});
it('metaMaskPrivateKey', async() => {
const w = HdWallet.fromMnemonic(mnemonic);
console.log('MetaMask sk:', w.metaMaskPrivateKey(0));
assert('6aad31c479c44230721b470570c12bd3f41e71b79d8f27ca08b913cbaeac25af'.toLowerCase() ===
w.metaMaskPrivateKey(0).toLowerCase());
});
})
See a complete project in the hdwallet branch of the field-support repo.
Make sure you have Node.js and NPM installed.
Open up a terminal and execute the following, assuming you hav
npx mnemonic-to-private-key "paste your 12 word phrase here".
Do NOT use online sites as you never know if they will store your phrase. In fact, even the above repository of "mnemonic-to-private-key" could be compromised, as the "Great Suspender" was. So it's best to use version pinning with npm.

Delayed dialog with resume handler

I have the following scenario I think I'm doing it wrong.
I have a RootDialog which calls a ResultDialog. The ResultDialog presents the user a list of results (using HeroCard).
The ResultDialog closes itself using context.Done(..) after the message was posted.
In the RootDialog- AfterResultDialog Resume handler I want to ask the user if he has found the matching result, using another dialog (NotificationDialog), but I want to do that after 30 seconds.
After some research, this seems like it must be done using proactive messages.
It this example, I found a way to post the NotificationDialog in a proactive way.
private async Task AfterResultDialog(IDialogContext context, IAwaitable<object> result)
{
var message = await result as IMessageActivity;
var conversationReference = context.Activity.ToConversationReference();
ConversationStarter.conversationReference = JsonConvert.SerializeObject(conversationReference);
t = new Timer(timerEvent);
t.Change(30000, Timeout.Infinite);
context.Wait<string>(NotificationDialogAfter);
}
public void timerEvent(object target)
{
t.Dispose();
ConversationStarter.Resume();
}
But the problem I have is that I'm interested in the result of this NotifcationDialog to know what the user wants to do next.
But all examples I found using proactive-messages do not regard the result of a proactive message with dialog:
public class ConversationStarter
{
public static string conversationReference;
public static async Task Resume()
{
var message = JsonConvert.DeserializeObject<ConversationReference>(conversationReference).GetPostToBotMessage();
var client = new ConnectorClient(new Uri(message.ServiceUrl));
using (var scope = DialogModule.BeginLifetimeScope(Conversation.Container, message))
{
var botData = scope.Resolve<IBotData>();
await botData.LoadAsync(CancellationToken.None);
var task = scope.Resolve<IDialogTask>();
// here it seems only to be possible to call a dialog using Void
var dialog = new NotificationDialog();
task.Call(dialog.Void<object, IMessageActivity>(), null);
await task.PollAsync(CancellationToken.None);
await botData.FlushAsync(CancellationToken.None);
}
}
}
The NotificationDialogAfter handler should decide based on the user input which dialog to call next:
private async Task NotificationDialogAfter(IDialogContext context, IAwaitable<string> result)
{
var whereToContinue = await result;
if (whereToContinue.Equals("Start over"))
{
context.ClearAllConversationDataKeys();
context.Call(new TagDialog(), this.TagDialogAfter);
}
else if (whereToContinue == "Tell friends")
{
context.Call(new TellFriendsDialog(), TellFriendsDialogAfter);
}
else if (whereToContinue == "Feedback")
{
context.Call(new FeedbackDialog(), this.FeedbackDialogAfter);
}
}
So what I basically want is that the result of the NotificationDialog is forwarded to the NotificationDialogAfter handler which the Root dialog is waiting for.
Is this even possible?
I solve the problem by defining static continue handlers (in GlobalContinueHandler), that I can provide inside the NotificationDialog, when calling other dialogs.
[Serializable]
public class NotificationDialog : IDialog<string>
{
public Task StartAsync(IDialogContext context)
{
PromptDialog.Choice(context, Resume, new List<string> { "yes", "no" },
"Found what you're looking for?");
return Task.CompletedTask;
}
private async Task Resume(IDialogContext context, IAwaitable<string> result)
{
var message = await result;
if (message == "yes")
{
context.Call(new SignupDialog(), GlobalContinueHandler.SignupDialogAfter);
}
else
{
context.Call(new FeedbackDialog(), GlobalContinueHandler.FeedbackDialogAfter);
}
}
}
I'm really not fan of this solution but for now it seems to work.

Windows 8.1 store apps OnCommandsRequested doesn't add ApplicationCommands when async used

On the App.xaml.cs I have the following code
private async void OnCommandsRequested(SettingsPane settingsPane, SettingsPaneCommandsRequestedEventArgs e)
{
var loader = ResourceLoader.GetForCurrentView();
var generalCommand = new SettingsCommand("General Settings", "General Settings", handler =>
{
var generalSettings = new GeneralSettingsFlyout();
generalSettings.Show();
});
e.Request.ApplicationCommands.Add(generalCommand);
object data;
IAuthService _authService = new AuthService();
if (Global.UserId == 0)
data = await _authService.GetSettingValueBySettingName(DatabaseType.GeneralDb, ApplicationConstants.GeneralDbSettingNames.ShowSupportInfo);
else
data = await _authService.GetSettingValueBySettingName(DatabaseType.UserDb, ApplicationConstants.UserDbSettingNames.ShowSupportInfo);
if (data != null && data.ToString().Equals("1"))
{
var supportCommand = new SettingsCommand("Support", "Support", handler =>
{
var supportPane = new SupportFlyout();
supportPane.Show();
});
e.Request.ApplicationCommands.Add(supportCommand);
}
var aboutCommand = new SettingsCommand("About", loader.GetString("Settings_OptionLabels_About"), handler =>
{
var aboutPane = new About();
aboutPane.Show();
});
e.Request.ApplicationCommands.Add(aboutCommand);
}
This code adds the setting "General Settings" but neither "Support" or "About" commands. Can anyone advice what's wrong with this code?
Instead of querying the commands from your service when they are requested you'll need to query them ahead of time and then add the already known commands.
You cannot use await in OnCommandsRequested.
A method returns when it gets to the first await, so only commands added to the request before the await will be used.
Since the SettingsPaneCommandsRequestedEventArgs doesn't provide a deferral there is no way to tell the requester to wait for internal async calls to complete.
Note also that SettingsPane is deprecated and not recommended for new app development for Windows 10.

Resources