Te tuhi toronga tirotiro haumaru

Te tuhi toronga tirotiro haumaru

Kaore i rite ki te hoahoanga "kiritaki-tumau" noa, ko nga tono whakahekenga e tohuhia ana e:

  • Kaore he take ki te penapena i tetahi papaa raraunga me nga whakaurunga kaiwhakamahi me nga kupuhipa. Ko nga korero uru e rongoa ana ma nga kaiwhakamahi anake, a ka puta te whakapumautanga o to ratou pono i te taumata kawa.
  • Kaore he take ki te whakamahi i te tūmau. Ka taea te whakahaere i te arorau tono i runga i te whatunga poraka, ka taea te penapena i te nui o nga raraunga e hiahiatia ana.

E rua nga putunga haumaru mo nga taviri kaiwhakamahi - nga putea taputapu me nga toronga tirotiro. Ko nga putea taputapu he tino haumaru te nuinga, engari he uaua ki te whakamahi me te matara mai i te kore utu, engari ko nga toronga tirotiro te whakakotahitanga tino pai o te haumarutanga me te ngawari o te whakamahi, ka taea hoki te kore utu mo nga kaiwhakamahi mutunga.

Ma te whai whakaaro ki enei mea katoa, i pirangi matou ki te hanga i te toronga tino haumaru e whakamaarama ana i te whakawhanaketanga o nga tono whakaheke ma te whakarato i tetahi API ngawari mo te mahi me nga whakawhitinga me nga hainatanga.
Ka korerotia e matou ki a koe mo tenei wheako i raro nei.

Kei roto i te tuhinga nga tohutohu taahiraa-i-taahiraa mo te tuhi i te toronga tirotiro, me nga tauira waehere me nga whakaahua. Ka kitea e koe nga waehere katoa i roto whare putunga. Ko ia mahi e rite ana ki tetahi waahanga o tenei tuhinga.

He Hītori Poto mo nga Toronga Pūtirotiro

Kua roa nga toronga tirotiro tirotiro. I puta mai i Internet Explorer i te tau 1999, i Firefox i te tau 2004. Heoi, mo te wa roa kaore he paerewa kotahi mo nga toronga.

Ka taea e maatau te kii i puta mai me nga taapiri i te wha o nga putanga o Google Chrome. Ae ra, karekau he korero i tera wa, engari ko te Chrome API te turanga: kua riro i a ia te nuinga o te maakete tirotiro me te whai toa tono whakaurunga, na Chrome i whakatakoto te paerewa mo nga toronga tirotiro.

Kei a Mozilla tana ake paerewa, engari i te kitenga i te rongonui o nga toronga Chrome, ka whakatau te kamupene ki te hanga API hototahi. I te tau 2015, na Mozilla, i hangaia he roopu motuhake i roto i te World Wide Web Consortium (W3C) ki te mahi i nga whakaritenga toronga whakawhiti-whakawhiti.

Ko nga taapiri API o mua mo Chrome i tangohia hei kaupapa. I whakatutukihia nga mahi me te tautoko a Microsoft (kaore a Google i whakaae ki te whakauru ki te whakawhanaketanga o te paerewa), na reira ka puta mai he tauira. whakaritenga.

Ko te tikanga, kei te tautokohia e Edge, Firefox me Opera te tohu (me mohio kaore a Chrome i tenei rarangi). Engari ko te tikanga, ko te paerewa he hototahi ki a Chrome, na te mea kua tuhia i runga i ona toronga. Ka taea e koe te panui atu mo te WebExtensions API konei.

Hanganga toronga

Ko te kōnae anake e hiahiatia ana mo te toronga ko te whakaaturanga (manifest.json). Koia hoki te "whakauru" ki te whakawhänui.

Whakaaturanga

E ai ki nga korero, ko te konae whakaatu he konae JSON whaimana. He whakaahuatanga katoa o nga taviri whakaatu me nga korero e pa ana ki nga taviri e tautokohia ana e taea ai te tirotiro konei.

Ko nga taviri kaore i roto i te tohu "ka" ka warewarehia (ka puta nga hapa a Chrome me Firefox, engari kei te mahi tonu nga toronga).

A ka hiahia ahau ki te kukume i te aro ki etahi waahanga.

  1. papamuri — he ahanoa kei roto nga mara e whai ake nei:
    1. hōtuhi — he huinga tuhinga ka mahia i roto i te horopaki papamuri (ka korero tatou mo tenei i muri tata nei);
    2. Whārangi - hei utu mo nga tuhinga ka mahia ki te wharangi kau, ka taea e koe te tohu html me nga ihirangi. I tenei keehi, ka warewarehia te mara tuhinga, me whakauru nga tuhinga ki te wharangi ihirangi;
    3. tohe - he haki rua, ki te kore e tohua, ka "whakamate" te kaitirotiro i te mahi papamuri ina whakaaro ana kaore he mahi, ka whakaara ano mena ka tika. Ki te kore, ka tukuna noa te wharangi ina katia te kaitirotiro. Kaore i te tautokohia i Firefox.
  2. ihirangi_whakatuhi — he huinga taonga ka taea e koe te uta i nga tuhinga rereke ki nga wharangi paetukutuku rereke. Kei ia ahanoa nga waahanga nui e whai ake nei:
    1. kēmu - tauira url, e whakatau ana mena ka whakaurua tetahi tuhinga ihirangi, kaore ranei.
    2. js — he rarangi o nga tuhinga ka utaina ki tenei whakataetae;
    3. whakakore_matches - ka whakakore i te mara match URL e taurite ana ki tenei mara.
  3. wharangi_mahi - he ahanoa te kawenga mo te tohu ka whakaatuhia ki te taha o te pae wāhitau i roto i te tirotiro me te taunekeneke ki a ia. Ka taea hoki e koe te whakaatu i tetahi matapihi pakūake, kua tautuhia ma te whakamahi i to HTML, CSS me JS.
    1. taunoa_popup — ara ki te konae HTML me te atanga pakūake, kei roto pea te CSS me te JS.
  4. whakaaetanga — he huinga mo te whakahaere motika toronga. E 3 nga momo motika, e whakamaramahia ana konei
  5. web_accessible_resources — rauemi toronga ka taea e te wharangi paetukutuku te tono, hei tauira, whakaahua, JS, CSS, konae HTML.
  6. externally_connectable — i konei ka taea e koe te whakaatu i nga TT o etahi atu toronga me nga rohe o nga wharangi tukutuku ka taea e koe te hono atu. Ka taea e te rohe te taumata tuarua, teitei ake ranei. Kaore e mahi i Firefox.

Te horopaki mahi

Ko te toronga e toru nga horopaki mahi waehere, ara, ko te tono e toru nga waahanga me nga taumata rereke o te uru ki te API tirotiro.

Te horopaki toronga

Kei konei te nuinga o te API. I tenei horopaki ka "ora" ratou:

  1. Whārangi papamuri — "muri" waahanga o te toronga. Ka tohua te konae ki te whakaaturanga ma te whakamahi i te taviri "papamuri".
  2. Whārangi pahūake — he wharangi pakūake ka puta ina paato koe i te ata toronga. I roto i te whakaaturanga browser_action -> default_popup.
  3. Wharangi ritenga — wharangi toronga, “noho” ki tetahi ripa motuhake o te tirohanga chrome-extension://<id_расширения>/customPage.html.

Ka noho motuhake tenei horopaki i nga matapihi tirotiro me nga ripa. Whārangi papamuri kei roto i te kape kotahi me te mahi tonu (koe ko te wharangi takahanga, ina whakarewahia te tuhinga papamuri e tetahi huihuinga me te "mate" i muri i tana mahi). Whārangi pahūake ka noho ina tuwhera te matapihi pakūake, a Wharangi ritenga — i te wa e tuwhera ana te ripa kei a ia. Karekau he uru ki etahi atu ripa me o raatau ihirangi mai i tenei horopaki.

Te horopaki tuhinga ihirangi

Ka whakarewahia te konae tuhinga tuhinga me ia ripa tirotiro. Ka whai waahi ki tetahi waahanga o te API o te toronga me te rakau DOM o te wharangi paetukutuku. Ko nga tuhinga tuhinga te kawenga mo te taunekeneke me te whaarangi. Ko nga toronga e raweke ana i te rakau DOM ka mahi i tenei i roto i nga tuhinga tuhinga - hei tauira, nga aukati panui, kaiwhakamaori ranei. Ano, ka taea e te tuhinga tuhinga te korero ki te whaarangi ma te paerewa postMessage.

Te horopaki whārangi tukutuku

Ko te wharangi paetukutuku tonu tenei. Karekau he hononga ki te toronga, karekau he uru ki reira, engari ko nga keehi karekau i tino whakaatuhia te rohe o tenei wharangi i roto i te whakaaturanga (he korero kei raro nei).

Whakawhiti Karere

Ko nga waahanga rereke o te tono me whakawhiti korero ki a raatau ano. He API mo tenei runtime.sendMessage ki te tuku karere background и tabs.sendMessage ki te tuku karere ki tetahi wharangi (whakatuhi ihirangi, pahū-ake, wharangi paetukutuku mena kei te waatea externally_connectable). Kei raro nei he tauira ina uru ki te Chrome API.

// Сообщением может быть любой JSON сериализуемый объект
const msg = {a: 'foo', b: 'bar'};

// extensionId можно не указывать, если мы хотим послать сообщение 'своему' расширению (из ui или контент скрипта)
chrome.runtime.sendMessage(extensionId, msg);

// Так выглядит обработчик
chrome.runtime.onMessage.addListener((msg) => console.log(msg))

// Можно слать сообщения вкладкам зная их id
chrome.tabs.sendMessage(tabId, msg)

// Получить к вкладкам и их id можно, например, вот так
chrome.tabs.query(
    {currentWindow: true, active : true},
    function(tabArray){
      tabArray.forEach(tab => console.log(tab.id))
    }
)

Mo te whakawhitiwhiti korero katoa, ka taea e koe te hanga hononga ma runtime.connect. Hei whakautu ka whiwhi tatou runtime.Port, i te wa e tuwhera ana, ka taea e koe te tuku karere maha. I te taha o te kiritaki, hei tauira, contentscript, penei te ahua:

// Опять же extensionId можно не указывать при коммуникации внутри одного расширения. Подключение можно именовать
const port = chrome.runtime.connect({name: "knockknock"});
port.postMessage({joke: "Knock knock"});
port.onMessage.addListener(function(msg) {
    if (msg.question === "Who's there?")
        port.postMessage({answer: "Madame"});
    else if (msg.question === "Madame who?")
        port.postMessage({answer: "Madame... Bovary"});

Tūmau, papamuri rānei:

// Обработчик для подключения 'своих' вкладок. Контент скриптов, popup или страниц расширения
chrome.runtime.onConnect.addListener(function(port) {
    console.assert(port.name === "knockknock");
    port.onMessage.addListener(function(msg) {
        if (msg.joke === "Knock knock")
            port.postMessage({question: "Who's there?"});
        else if (msg.answer === "Madame")
            port.postMessage({question: "Madame who?"});
        else if (msg.answer === "Madame... Bovary")
            port.postMessage({question: "I don't get it."});
    });
});

// Обработчик для подключения внешних вкладок. Других расширений или веб страниц, которым разрешен доступ в манифесте
chrome.runtime.onConnectExternal.addListener(function(port) {
    ...
});

He huihuinga ano onDisconnect me te tikanga disconnect.

Hoahoa tono

Me hanga he toronga tirotiro e pupuri ana i nga taviri motuhake, ka uru atu ki nga korero a te iwi (te wahitau, te taviri a te iwi ka korero ki te wharangi me te tuku i nga tono tuatoru ki te tono haina mo nga tauwhitinga.

Te whanaketanga tono

Ko ta maatau tono me mahi tahi me te kaiwhakamahi me te whakarato i te whaarangi he API hei karanga tikanga (hei tauira, ki te haina i nga tauwhitinga). Kia kotahi noa contentscript e kore e mahi, na te mea ka uru noa ki te DOM, engari kaua ki te JS o te wharangi. Hono mā runtime.connect e kore e taea e matou, na te mea e hiahiatia ana te API ki nga rohe katoa, a ko nga mea motuhake anake ka taea te tohu i roto i te whakaaturanga. Ko te mutunga, ka penei te ahua o te hoahoa:

Te tuhi toronga tirotiro haumaru

Ka puta ano tetahi tuhinga - inpage, ka werohia e matou ki te wharangi. Ka rere i roto i tona horopaki me te whakarato i te API mo te mahi me te toronga.

Начало

Kei te waatea nga waehere toronga tirotiro katoa i GitHub. I te wa o te whakamaarama ka whai hononga ki nga mahi.

Me timata me te whakaaturanga:

{
  // Имя и описание, версия. Все это будет видно в браузере в chrome://extensions/?id=<id расширения>
  "name": "Signer",
  "description": "Extension demo",
  "version": "0.0.1",
  "manifest_version": 2,

  // Скрипты, которые будут исполнятся в background, их может быть несколько
  "background": {
    "scripts": ["background.js"]
  },

  // Какой html использовать для popup
  "browser_action": {
    "default_title": "My Extension",
    "default_popup": "popup.html"
  },

  // Контент скрипты.
  // У нас один объект: для всех url начинающихся с http или https мы запускаем
  // contenscript context со скриптом contentscript.js. Запускать сразу по получении документа для всех фреймов
  "content_scripts": [
    {
      "matches": [
        "http://*/*",
        "https://*/*"
      ],
      "js": [
        "contentscript.js"
      ],
      "run_at": "document_start",
      "all_frames": true
    }
  ],
  // Разрешен доступ к localStorage и idle api
  "permissions": [
    "storage",
    // "unlimitedStorage",
    //"clipboardWrite",
    "idle"
    //"activeTab",
    //"webRequest",
    //"notifications",
    //"tabs"
  ],
  // Здесь указываются ресурсы, к которым будет иметь доступ веб страница. Тоесть их можно будет запрашивать fetche'м или просто xhr
  "web_accessible_resources": ["inpage.js"]
}

Waihangahia background.js kau, popup.js, inpage.js me contentscript.js. Ka taapirihia e matou te popup.html - ka taea te utaina to maatau tono ki a Google Chrome me te whakarite kei te mahi.

Hei manatoko i tenei, ka taea e koe te tango i te waehere mai i konei. I tua atu i ta maatau mahi, i whirihorahia e te hononga te huihuinga o te kaupapa ma te whakamahi i te kete tukutuku. Hei taapiri i tetahi tono ki te kaitirotiro, i roto i te chrome://extensions ka hiahia koe ki te kowhiri i te utaina kua wetewetehia me te kōpaki me te toronga e rite ana - i roto i ta maatau keehi.

Te tuhi toronga tirotiro haumaru

Inaianei kua whakauruhia ta maatau toronga me te mahi. Ka taea e koe te whakahaere i nga taputapu kaiwhakawhanake mo nga horopaki rereke penei:

pakūake ->

Te tuhi toronga tirotiro haumaru

Ko te uru ki te papatohu tuhinga ihirangi ka mahia ma te papatohu o te wharangi ake i whakarewahia ai.Te tuhi toronga tirotiro haumaru

Whakawhiti Karere

Na, me whakatu e rua nga hongere korero: inpage <-> papamuri me te popup <-> papamuri. Ka taea e koe te tuku karere noa ki te tauranga me te hanga i to ake kawa, engari he pai ki ahau te huarahi i kitea e ahau i roto i te kaupapa puna tuwhera metamask.

He toronga tirotiro mo te mahi me te whatunga Ethereum. I roto, ko nga waahanga rereke o te tono korero ma te RPC ma te whakamahi i te whare pukapuka dnode. Ka taea e koe te whakarite i tetahi whakawhitiwhitinga tere me te ngawari mena ka tukuna e koe he awa nodejs hei kawe (te tikanga he ahanoa e whakamahi ana i te atanga kotahi):

import Dnode from "dnode/browser";

// В этом примере условимся что клиент удаленно вызывает функции на сервере, хотя ничего нам не мешает сделать это двунаправленным

// Cервер
// API, которое мы хотим предоставить
const dnode = Dnode({
    hello: (cb) => cb(null, "world")
})
// Транспорт, поверх которого будет работать dnode. Любой nodejs стрим. В браузере есть бибилиотека 'readable-stream'
connectionStream.pipe(dnode).pipe(connectionStream)

// Клиент
const dnodeClient = Dnode() // Вызов без агрумента значит что мы не предоставляем API на другой стороне

// Выведет в консоль world
dnodeClient.once('remote', remote => {
    remote.hello(((err, value) => console.log(value)))
})

Inaianei ka hangaia e matou he karaehe tono. Ka hangaia he ahanoa API mo te pahū-ake me te wharangi paetukutuku, ka hangaia he dnode mo ratou:

import Dnode from 'dnode/browser';

export class SignerApp {

    // Возвращает объект API для ui
    popupApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Возвращает объет API для страницы
    pageApi(){
        return {
            hello: cb => cb(null, 'world')
        }
    }

    // Подключает popup ui
    connectPopup(connectionStream){
        const api = this.popupApi();
        const dnode = Dnode(api);

        connectionStream.pipe(dnode).pipe(connectionStream);

        dnode.on('remote', (remote) => {
            console.log(remote)
        })
    }

    // Подключает страницу
    connectPage(connectionStream, origin){
        const api = this.popupApi();
        const dnode = Dnode(api);

        connectionStream.pipe(dnode).pipe(connectionStream);

        dnode.on('remote', (remote) => {
            console.log(origin);
            console.log(remote)
        })
    }
}

I konei me raro, hei utu mo te ahanoa Chrome o te ao, ka whakamahia e matou te extensionApi, e uru ana ki a Chrome i roto i te tirotiro a Google me te tirotiro i etahi atu. Ka mahia tenei mo te hototahitanga whakawhiti-tirotiro, engari mo nga kaupapa o tenei tuhinga, ka taea noa e koe te whakamahi 'chrome.runtime.connect'.

Me hanga he tauira tono ki te tuhinga papamuri:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";

const app = new SignerApp();

// onConnect срабатывает при подключении 'процессов' (contentscript, popup, или страница расширения)
extensionApi.runtime.onConnect.addListener(connectRemote);

function connectRemote(remotePort) {
    const processName = remotePort.name;
    const portStream = new PortStream(remotePort);
    // При установке соединения можно указывать имя, по этому имени мы и оппределяем кто к нам подлючился, контентскрипт или ui
    if (processName === 'contentscript'){
        const origin = remotePort.sender.url
        app.connectPage(portStream, origin)
    }else{
        app.connectPopup(portStream)
    }
}

I te mea ka mahi tahi te dnode me nga awa, a ka whiwhi matou i tetahi tauranga, ka hiahiatia he akomanga urutau. Ka mahia ma te whakamahi i te whare pukapuka awa-panui, e whakatinana ana i nga awa nodejs i roto i te tirotiro:

import {Duplex} from 'readable-stream';

export class PortStream extends Duplex{
    constructor(port){
        super({objectMode: true});
        this._port = port;
        port.onMessage.addListener(this._onMessage.bind(this));
        port.onDisconnect.addListener(this._onDisconnect.bind(this))
    }

    _onMessage(msg) {
        if (Buffer.isBuffer(msg)) {
            delete msg._isBuffer;
            const data = new Buffer(msg);
            this.push(data)
        } else {
            this.push(msg)
        }
    }

    _onDisconnect() {
        this.destroy()
    }

    _read(){}

    _write(msg, encoding, cb) {
        try {
            if (Buffer.isBuffer(msg)) {
                const data = msg.toJSON();
                data._isBuffer = true;
                this._port.postMessage(data)
            } else {
                this._port.postMessage(msg)
            }
        } catch (err) {
            return cb(new Error('PortStream - disconnected'))
        }
        cb()
    }
}

Inaianei me hanga hononga ki te UI:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import Dnode from 'dnode/browser';

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupUi().catch(console.error);

async function setupUi(){
    // Также, как и в классе приложения создаем порт, оборачиваем в stream, делаем  dnode
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    const dnode = Dnode();

    connectionStream.pipe(dnode).pipe(connectionStream);

    const background = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

    // Делаем объект API доступным из консоли
    if (DEV_MODE){
        global.background = background;
    }
}

Na ka hangaia e matou te hononga ki te tuhinga ihirangi:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import PostMessageStream from 'post-message-stream';

setupConnection();
injectScript();

function setupConnection(){
    const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
    const backgroundStream = new PortStream(backgroundPort);

    const pageStream = new PostMessageStream({
        name: 'content',
        target: 'page',
    });

    pageStream.pipe(backgroundStream).pipe(pageStream);
}

function injectScript(){
    try {
        // inject in-page script
        let script = document.createElement('script');
        script.src = extensionApi.extension.getURL('inpage.js');
        const container = document.head || document.documentElement;
        container.insertBefore(script, container.children[0]);
        script.onload = () => script.remove();
    } catch (e) {
        console.error('Injection failed.', e);
    }
}

I te mea e hiahia ana matou ki te API kaore i roto i te tuhinga tuhinga, engari i runga tonu i te wharangi, e rua nga mea ka mahia e matou:

  1. Ka hangaia e matou nga awa e rua. Kotahi - ki te wharangi, kei runga o te Karere panui. Mo tenei ka whakamahia e matou tenei tenei kete mai i nga kaihanga o metamask. Ko te awa tuarua ko te papamuri i runga i te tauranga i riro mai runtime.connect. Me hoko e tatou. Inaianei ka whai awa te wharangi ki te papamuri.
  2. Tuhia te tuhinga ki te DOM. Tikiake i te hōtuhi (i whakaaetia te uru ki roto i te whakaaturanga) ka hangaia he tohu script me ona ihirangi o roto:

import PostMessageStream from 'post-message-stream';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";

setupConnection();
injectScript();

function setupConnection(){
    // Стрим к бекграунду
    const backgroundPort = extensionApi.runtime.connect({name: 'contentscript'});
    const backgroundStream = new PortStream(backgroundPort);

    // Стрим к странице
    const pageStream = new PostMessageStream({
        name: 'content',
        target: 'page',
    });

    pageStream.pipe(backgroundStream).pipe(pageStream);
}

function injectScript(){
    try {
        // inject in-page script
        let script = document.createElement('script');
        script.src = extensionApi.extension.getURL('inpage.js');
        const container = document.head || document.documentElement;
        container.insertBefore(script, container.children[0]);
        script.onload = () => script.remove();
    } catch (e) {
        console.error('Injection failed.', e);
    }
}

Inaianei ka hangaia e matou he ahanoa api ki roto i te wharangi me te whakanoho ki te ao:

import PostMessageStream from 'post-message-stream';
import Dnode from 'dnode/browser';

setupInpageApi().catch(console.error);

async function setupInpageApi() {
    // Стрим к контентскрипту
    const connectionStream = new PostMessageStream({
        name: 'page',
        target: 'content',
    });

    const dnode = Dnode();

    connectionStream.pipe(dnode).pipe(connectionStream);

    // Получаем объект API
    const pageApi = await new Promise(resolve => {
        dnode.once('remote', api => {
            resolve(api)
        })
    });

    // Доступ через window
    global.SignerApp = pageApi;
}

Kua reri matou Waea Tikanga Mamao (RPC) me te API motuhake mo te wharangi me te UI. Ina hono ana i tetahi wharangi hou ki te papamuri ka kite tatou i tenei:

Te tuhi toronga tirotiro haumaru

Putua API me te takenga. I te taha wharangi, ka taea e tatou te karanga i te mahi hello penei:

Te tuhi toronga tirotiro haumaru

Ko te mahi me nga mahi waea whakahoki i roto i te JS hou he ahua kino, na me tuhi he kaiawhina iti hei hanga i tetahi dnode e taea ai e koe te tuku i tetahi mea API ki nga taputapu.

Ka penei te ahua o nga mea API:

export class SignerApp {

    popupApi() {
        return {
            hello: async () => "world"
        }
    }

...

}

Te tiki ahanoa mai i tawhiti penei:

import {cbToPromise, transformMethods} from "../../src/utils/setupDnode";

const pageApi = await new Promise(resolve => {
    dnode.once('remote', remoteApi => {
        // С помощью утилит меняем все callback на promise
        resolve(transformMethods(cbToPromise, remoteApi))
    })
});

A ko nga mahi karanga ka hoki mai he kupu whakaari:

Te tuhi toronga tirotiro haumaru

Putanga me nga mahi tukutahi e waatea ana konei.

I te katoa, he ngawari te ahua o te huarahi RPC me te awa: ka taea e tatou te whakamahi i te tini o te mamaoa me te hanga i etahi momo API mo nga mahi rereke. Ko te tikanga, ka taea te whakamahi i te dnode ki hea, ko te mea nui ko te takai i te kawe i te ahua o te awa nodejs.

Ko tetahi atu ko te whakatakotoranga JSON, e whakatinana ana i te kawa JSON RPC 2. Heoi, ka mahi tahi me nga kawe waka motuhake (TCP me HTTP(S)), kaore e pa ana ki a maatau.

Te rohe o roto me te Rokiroki rohe

Me pupuri e matou te ahua o roto o te tono - ko nga taviri hainatanga. Ka taea e taatau te taapiri i tetahi ahuatanga ki te tono me nga tikanga mo te whakarereke i roto i te API pakūake:

import {setupDnode} from "./utils/setupDnode";

export class SignerApp {

    constructor(){
        this.store = {
            keys: [],
        };
    }

    addKey(key){
        this.store.keys.push(key)
    }

    removeKey(index){
        this.store.keys.splice(index,1)
    }

    popupApi(){
        return {
            addKey: async (key) => this.addKey(key),
            removeKey: async (index) => this.removeKey(index)
        }
    }

    ...

} 

I te papamuri, ka takai tatou i nga mea katoa ki tetahi mahi ka tuhia te ahanoa tono ki te matapihi kia taea ai e tatou te mahi mai i te papatohu:

import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupApp();

function setupApp() {
    const app = new SignerApp();

    if (DEV_MODE) {
        global.app = app;
    }

    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url;
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

Me taapiri etahi taviri mai i te papatohu UI me te kite he aha te tupu ki te kawanatanga:

Te tuhi toronga tirotiro haumaru

Me kaha tonu te kawanatanga kia kore ai e ngaro nga taviri ina timata ano.

Ka penapenahia e matou ki roto i te Roopu Roopu, ka tuhirua me nga huringa katoa. I muri mai, me whai waahi ano mo te UI, ka hiahia ano ahau ki te ohauru ki nga huringa. I runga i tenei, he pai ki te hanga i tetahi rokiroki ka kitea me te ohauru ki ona huringa.

Ka whakamahia e matou te whare pukapuka mobx (https://github.com/mobxjs/mobx). Ko te whiringa i taka ki runga na te mea kaore au e mahi ki a ia, engari i tino hiahia ahau ki te ako.

Me taapirihia te arawhitinga o te ahua tuatahi kia kitea te toa:

import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";

export class SignerApp {

    constructor(initState = {}) {
        // Внешне store так и останется тем же объектом, только теперь все его поля стали proxy, которые отслеживают доступ к ним
        this.store =  observable.object({
            keys: initState.keys || [],
        });
    }

    // Методы, которые меняют observable принято оборачивать декоратором
    @action
    addKey(key) {
        this.store.keys.push(key)
    }

    @action
    removeKey(index) {
        this.store.keys.splice(index, 1)
    }

    ...

}

"I raro i te awhi," kua whakakapihia e mobx nga mara toa katoa ki te takawaenga me te haukoti i nga waea katoa ki a raatau. Ka taea te ohauru ki enei karere.

Kei raro nei ka whakamahia e au te kupu "ka huri", ahakoa kaore tenei i te tino tika. Ka whai a Mobx te uru ki nga mara. Ka whakamahia nga kaiwhakawhiwhi me nga kaiwhakatakoto ahanoa takawaenga ka hangaia e te whare pukapuka.

E rua nga kaupapa o nga mahi whakapaipai:

  1. I roto i te aratau kaha me te haki enforceActions, ka aukati te mobx ki te whakarereke tika i te kawanatanga. E kiia ana he mahi pai te mahi i raro i nga tikanga taumaha.
  2. Ahakoa he maha nga wa ka huri te mahi - hei tauira, he maha nga waahi ka hurihia i roto i nga rarangi waehere - ka whakamohiohia nga kaitirotiro ka oti ana. He mea tino nui tenei mo te pito o mua, kei reira nga whakahōutanga whenua kore e tika ana ki te whakaputa huanga kore. I roto i to maatau, kaore te tuatahi me te tuarua e tino whai kiko ana, engari ka whai maatau i nga tikanga pai. He tikanga ki te taapiri i nga whakapaipai ki nga mahi katoa e whakarereke ana i te ahua o nga mara kua kitea.

I te papamuri ka taapirihia te arawhiti me te penapena i te ahua ki te localStorage:

import {reaction, toJS} from 'mobx';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
// Вспомогательные методы. Записывают/читают объект в/из localStorage виде JSON строки по ключу 'store'
import {loadState, saveState} from "./utils/localStorage";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupApp();

function setupApp() {
    const initState = loadState();
    const app = new SignerApp(initState);

    if (DEV_MODE) {
        global.app = app;
    }

    // Setup state persistence

    // Результат reaction присваивается переменной, чтобы подписку можно было отменить. Нам это не нужно, оставлено для примера
    const localStorageReaction = reaction(
        () => toJS(app.store), // Функция-селектор данных
        saveState // Функция, которая будет вызвана при изменении данных, которые возвращает селектор
    );

    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

He mea whakamere te mahi tauhohenga i konei. E rua nga tohenga:

  1. Kaiwhiri raraunga.
  2. He kaihautu ka karangahia me enei raraunga i nga wa katoa ka huri.

Kaore i rite ki te redux, i reira ka whakawhiwhia e matou te kawanatanga hei tohenga, ka maumahara a mobx ko nga tirohanga ka uru atu matou ki roto i te kaiwhiriwhiri, ka waea atu ki te kaihautu ina huri ana.

He mea nui kia maarama me pehea te whakatau a mobx ko wai nga tirohanga ka ohauru tatou. Mena i tuhia e au he kaikowhiri i roto i te waehere penei() => app.store, ka kore e kiia te tauhohenga, na te mea kaore e kitea te rokiroki, ko ona mara anake.

Mena ka penei taku tuhi () => app.store.keys, katahi ano kaore he mea e tupu, na te mea ka taapiri/tangohia nga huānga huānga, karekau te korero e huri.

Ka mahi a Mobx hei kaiwhiriwhiri mo te wa tuatahi, ka pupuri noa i nga kitenga kua uru atu ki a maatau. Ka mahia tenei ma nga kaiwhakawhiwhi takawaenga. Na reira, ka whakamahia te mahi hanga-i konei toJS. Ka whakahokia he ahanoa hou me nga takawaenga katoa kua whakakapia ki nga mara taketake. I te wa e mahia ana, ka panuihia nga mara katoa o te ahanoa - no reira ka puta nga kaipatu.

I roto i te papatohu pakūake ka taapirihia ano e matou etahi taviri. I tenei wa ka uru ano ratou ki te localStorage:

Te tuhi toronga tirotiro haumaru

Ina utaina ano te wharangi papamuri, ka mau tonu nga korero.

Ko nga waehere tono katoa tae noa ki tenei waahi ka taea te tiro konei.

Pupuri haumaru o nga taviri tūmataiti

Ko te pupuri i nga taviri motuhake i roto i nga tuhinga maamaa kaore i te haumaru: he tupono ka taumanutia koe, ka uru atu ki to rorohiko, aha atu. Na reira, i roto i te localStorage ka penapenahia e matou nga taviri ki te puka whakamunatia-kupuhipa.

Mo te nui ake o te haumarutanga, ka taapirihia e matou he ahua raka ki te tono, karekau he uru ki nga kii. Ka whakawhiti aunoatia e matou te toronga ki te ahua maukati na te mea he wa poto.

Ka taea e Mobx te penapena i nga huinga raraunga iti noa iho, ko te toenga ka tatau aunoa i runga i tera. Ko enei nga mea e kiia ana ko nga taonga rorohiko. Ka taea te whakataurite ki nga tirohanga i roto i nga papaa raraunga:

import {observable, action} from 'mobx';
import {setupDnode} from "./utils/setupDnode";
// Утилиты для безопасного шифрования строк. Используют crypto-js
import {encrypt, decrypt} from "./utils/cryptoUtils";

export class SignerApp {
    constructor(initState = {}) {
        this.store = observable.object({
            // Храним пароль и зашифрованные ключи. Если пароль null - приложение locked
            password: null,
            vault: initState.vault,

            // Геттеры для вычислимых полей. Можно провести аналогию с view в бд.
            get locked(){
                return this.password == null
            },
            get keys(){
                return this.locked ?
                    undefined :
                    SignerApp._decryptVault(this.vault, this.password)
            },
            get initialized(){
                return this.vault !== undefined
            }
        })
    }
    // Инициализация пустого хранилища новым паролем
    @action
    initVault(password){
        this.store.vault = SignerApp._encryptVault([], password)
    }
    @action
    lock() {
        this.store.password = null
    }
    @action
    unlock(password) {
        this._checkPassword(password);
        this.store.password = password
    }
    @action
    addKey(key) {
        this._checkLocked();
        this.store.vault = SignerApp._encryptVault(this.store.keys.concat(key), this.store.password)
    }
    @action
    removeKey(index) {
        this._checkLocked();
        this.store.vault = SignerApp._encryptVault([
                ...this.store.keys.slice(0, index),
                ...this.store.keys.slice(index + 1)
            ],
            this.store.password
        )
    }

    ... // код подключения и api

    // private
    _checkPassword(password) {
        SignerApp._decryptVault(this.store.vault, password);
    }

    _checkLocked() {
        if (this.store.locked){
            throw new Error('App is locked')
        }
    }

    // Методы для шифровки/дешифровки хранилища
    static _encryptVault(obj, pass){
        const jsonString = JSON.stringify(obj)
        return encrypt(jsonString, pass)
    }

    static _decryptVault(str, pass){
        if (str === undefined){
            throw new Error('Vault not initialized')
        }
        try {
            const jsonString = decrypt(str, pass)
            return JSON.parse(jsonString)
        }catch (e) {
            throw new Error('Wrong password')
        }
    }
}

Inaianei ka penapenahia e matou nga kii whakamunatia me te kupuhipa. Ko nga mea katoa ka tatauhia. Ka mahia e matou te whakawhiti ki te whenua kua kati ma te tango i te kupuhipa mai i te kawanatanga. Ko te API whanui inaianei he tikanga mo te arawhiti i te rokiroki.

I tuhia mo te whakamunatanga taputapu whakamahi crypto-js:

import CryptoJS from 'crypto-js'

// Используется для осложнения подбора пароля перебором. На каждый вариант пароля злоумышленнику придется сделать 5000 хешей
function strengthenPassword(pass, rounds = 5000) {
    while (rounds-- > 0){
        pass = CryptoJS.SHA256(pass).toString()
    }
    return pass
}

export function encrypt(str, pass){
    const strongPass = strengthenPassword(pass);
    return CryptoJS.AES.encrypt(str, strongPass).toString()
}

export function decrypt(str, pass){
    const strongPass = strengthenPassword(pass)
    const decrypted = CryptoJS.AES.decrypt(str, strongPass);
    return decrypted.toString(CryptoJS.enc.Utf8)
}

He API mangere te kaitirotiro e taea ai e koe te ohauru ki tetahi huihuinga - nga huringa kawanatanga. State, pera, pea idle, active и locked. Mo te mangere ka taea e koe te whakarite i te waahi, ka maukati te wa e aukatia ana te OS ake. Ka huri ano matou i te kaiwhiriwhiri mo te penapena ki localStorage:

import {reaction, toJS} from 'mobx';
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {SignerApp} from "./SignerApp";
import {loadState, saveState} from "./utils/localStorage";

const DEV_MODE = process.env.NODE_ENV !== 'production';
const IDLE_INTERVAL = 30;

setupApp();

function setupApp() {
    const initState = loadState();
    const app = new SignerApp(initState);

    if (DEV_MODE) {
        global.app = app;
    }

    // Теперь мы явно узываем поле, которому будет происходить доступ, reaction отработает нормально
    reaction(
        () => ({
            vault: app.store.vault
        }),
        saveState
    );

    // Таймаут бездействия, когда сработает событие
    extensionApi.idle.setDetectionInterval(IDLE_INTERVAL);
    // Если пользователь залочил экран или бездействовал в течение указанного интервала лочим приложение
    extensionApi.idle.onStateChanged.addListener(state => {
        if (['locked', 'idle'].indexOf(state) > -1) {
            app.lock()
        }
    });

    // Connect to other contexts
    extensionApi.runtime.onConnect.addListener(connectRemote);

    function connectRemote(remotePort) {
        const processName = remotePort.name;
        const portStream = new PortStream(remotePort);
        if (processName === 'contentscript') {
            const origin = remotePort.sender.url
            app.connectPage(portStream, origin)
        } else {
            app.connectPopup(portStream)
        }
    }
}

Ko te waehere i mua i tenei taahiraa ko konei.

Nga mahi

Na, ka tae mai ki te mea tino nui: te hanga me te haina i nga whakawhitiwhitinga i runga i te poraka. Ka whakamahia e matou te WAVES poraka me te whare pukapuka ngaru-whakawhitinga.

Tuatahi, me taapiri atu ki te kawanatanga he momo karere e tika ana kia hainatia, katahi ka taapirihia nga tikanga mo te taapiri i tetahi karere hou, te whakau i te hainatanga, me te kore e whakaae:

import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions'
import {setupDnode} from "./utils/setupDnode";
import {decrypt, encrypt} from "./utils/cryptoUtils";

export class SignerApp {

    ...

    @action
    newMessage(data, origin) {
        // Для каждого сообщения создаем метаданные с id, статусом, выременем создания и тд.
        const message = observable.object({
            id: uuid(), // Идентификатор, используюю uuid
            origin, // Origin будем впоследствии показывать в интерфейсе
            data, //
            status: 'new', // Статусов будет четыре: new, signed, rejected и failed
            timestamp: Date.now()
        });
        console.log(`new message: ${JSON.stringify(message, null, 2)}`);

        this.store.messages.push(message);

        // Возвращаем промис внутри которого mobx мониторит изменения сообщения. Как только статус поменяется мы зарезолвим его
        return new Promise((resolve, reject) => {
            reaction(
                () => message.status, //Будем обсервить статус сообщеня
                (status, reaction) => { // второй аргумент это ссылка на сам reaction, чтобы его можно было уничтожть внутри вызова
                    switch (status) {
                        case 'signed':
                            resolve(message.data);
                            break;
                        case 'rejected':
                            reject(new Error('User rejected message'));
                            break;
                        case 'failed':
                            reject(new Error(message.err.message));
                            break;
                        default:
                            return
                    }
                    reaction.dispose()
                }
            )
        })
    }
    @action
    approve(id, keyIndex = 0) {
        const message = this.store.messages.find(msg => msg.id === id);
        if (message == null) throw new Error(`No msg with id:${id}`);
        try {
            message.data = signTx(message.data, this.store.keys[keyIndex]);
            message.status = 'signed'
        } catch (e) {
            message.err = {
                stack: e.stack,
                message: e.message
            };
            message.status = 'failed'
            throw e
        }
    }
    @action
    reject(id) {
        const message = this.store.messages.find(msg => msg.id === id);
        if (message == null) throw new Error(`No msg with id:${id}`);
        message.status = 'rejected'
    }

    ...
}

Ka tae mai he karere hou, ka taapirihia he metadata ki reira, mahia observable me te tapiri atu ki store.messages.

Ki te kore koe observable ma te ringa, katahi ka mahia e mobx ake ina taapiri atu nga karere ki te rarangi. Heoi, ka hangaia he ahanoa hou karekau he tohutoro ki a matou, engari ka hiahia matou mo te mahi ka whai ake.

I muri mai, ka whakahokia e matou he oati e whakatau ana ina huri te mana o te karere. Ka aro turukihia te mana e te tauhohenga, ka "mate" ka huri te mana.

Waehere tikanga approve и reject tino ngawari: ka huri noa i te mana o te karere, i muri i te hainatanga mena e tika ana.

Ka hoatu e matou te Whakaae me te whakakore ki te UI API, te Karere hou ki te API wharangi:

export class SignerApp {
    ...
    popupApi() {
        return {
            addKey: async (key) => this.addKey(key),
            removeKey: async (index) => this.removeKey(index),

            lock: async () => this.lock(),
            unlock: async (password) => this.unlock(password),
            initVault: async (password) => this.initVault(password),

            approve: async (id, keyIndex) => this.approve(id, keyIndex),
            reject: async (id) => this.reject(id)
        }
    }

    pageApi(origin) {
        return {
            signTransaction: async (txParams) => this.newMessage(txParams, origin)
        }
    }

    ...
}

Inaianei me ngana ki te haina i te tauwhitinga me te toronga:

Te tuhi toronga tirotiro haumaru

I te nuinga o te waa, kua rite nga mea katoa, ko nga mea katoa e toe ana taapiri UI ngawari.

UI

Me uru te atanga ki te ahua tono. I te taha UI ka mahia e matou observable ahua me te taapiri i tetahi mahi ki te API ka huri i tenei ahuatanga. Me tapiri atu observable ki te ahanoa API i riro mai i te papamuri:

import {observable} from 'mobx'
import {extensionApi} from "./utils/extensionApi";
import {PortStream} from "./utils/PortStream";
import {cbToPromise, setupDnode, transformMethods} from "./utils/setupDnode";
import {initApp} from "./ui/index";

const DEV_MODE = process.env.NODE_ENV !== 'production';

setupUi().catch(console.error);

async function setupUi() {
    // Подключаемся к порту, создаем из него стрим
    const backgroundPort = extensionApi.runtime.connect({name: 'popup'});
    const connectionStream = new PortStream(backgroundPort);

    // Создаем пустой observable для состояния background'a
    let backgroundState = observable.object({});
    const api = {
        //Отдаем бекграунду функцию, которая будет обновлять observable
        updateState: async state => {
            Object.assign(backgroundState, state)
        }
    };

    // Делаем RPC объект
    const dnode = setupDnode(connectionStream, api);
    const background = await new Promise(resolve => {
        dnode.once('remote', remoteApi => {
            resolve(transformMethods(cbToPromise, remoteApi))
        })
    });

    // Добавляем в background observable со стейтом
    background.state = backgroundState;

    if (DEV_MODE) {
        global.background = background;
    }

    // Запуск интерфейса
    await initApp(background)
}

I te mutunga ka timata taatau ki te whakaputa i te atanga tono. He tono urupare tenei. Ka tukuna noa te ahanoa papamuri ma te whakamahi i nga taputapu. He tika, he tika, ki te hanga i tetahi ratonga motuhake mo nga tikanga me te toa mo te kawanatanga, engari mo nga kaupapa o tenei tuhinga ka nui tenei:

import {render} from 'react-dom'
import App from './App'
import React from "react";

// Инициализируем приложение с background объектом в качест ве props
export async function initApp(background){
    render(
        <App background={background}/>,
        document.getElementById('app-content')
    );
}

Ma te mobx he tino ngawari ki te tiimata ki te whakaputa ina huri nga raraunga. Ka whakairihia noa e matou te kaitirotiro whakapaipai mai i te kete mobx-react i runga i te wae, ka karanga aunoatia te tuku ina huri nga kitenga e tohuhia ana e te waahanga. Kaore koe e hiahia ki tetahi mapStateToProps, hono ranei penei i te redux. Ka mahi nga mea katoa mai i te pouaka:

import React, {Component, Fragment} from 'react'
import {observer} from "mobx-react";
import Init from './components/Initialize'
import Keys from './components/Keys'
import Sign from './components/Sign'
import Unlock from './components/Unlock'

@observer // У Компонета с этим декоратом будет автоматически вызван метод render, если будут изменены observable на которые он ссылается
export default class App extends Component {

    // Правильно конечно вынести логику рендера страниц в роутинг и не использовать вложенные тернарные операторы,
    // и привязывать observable и методы background непосредственно к тем компонентам, которые их используют
    render() {
        const {keys, messages, initialized, locked} = this.props.background.state;
        const {lock, unlock, addKey, removeKey, initVault, deleteVault, approve, reject} = this.props.background;

        return <Fragment>
            {!initialized
                ?
                <Init onInit={initVault}/>
                :
                locked
                    ?
                    <Unlock onUnlock={unlock}/>
                    :
                    messages.length > 0
                        ?
                        <Sign keys={keys} message={messages[messages.length - 1]} onApprove={approve} onReject={reject}/>
                        :
                        <Keys keys={keys} onAdd={addKey} onRemove={removeKey}/>
            }
            <div>
                {!locked && <button onClick={() => lock()}>Lock App</button>}
                {initialized && <button onClick={() => deleteVault()}>Delete all keys and init</button>}
            </div>
        </Fragment>
    }
}

Ko nga waahanga e toe ana ka taea te tiro i roto i te waehere i roto i te kōpaki UI.

Inaianei kei roto i te karaehe tono me hanga e koe he kaikowhiri kawanatanga mo te UI me te whakamohio ki te UI ina huri ana. Ki te mahi i tenei, me taapiri he tikanga getState и reactionkaranga remote.updateState:

import {action, observable, reaction} from 'mobx';
import uuid from 'uuid/v4';
import {signTx} from '@waves/waves-transactions'
import {setupDnode} from "./utils/setupDnode";
import {decrypt, encrypt} from "./utils/cryptoUtils";

export class SignerApp {

    ...

    // public
    getState() {
        return {
            keys: this.store.keys,
            messages: this.store.newMessages,
            initialized: this.store.initialized,
            locked: this.store.locked
        }
    }

    ...

    //
    connectPopup(connectionStream) {
        const api = this.popupApi();
        const dnode = setupDnode(connectionStream, api);

        dnode.once('remote', (remote) => {
            // Создаем reaction на изменения стейта, который сделает вызовет удаленну процедуру и обновит стейт в ui процессе
            const updateStateReaction = reaction(
                () => this.getState(),
                (state) => remote.updateState(state),
                // Третьим аргументом можно передавать параметры. fireImmediatly значит что reaction выполниться первый раз сразу.
                // Это необходимо, чтобы получить начальное состояние. Delay позволяет установить debounce
                {fireImmediately: true, delay: 500}
            );
            // Удалим подписку при отключении клиента
            dnode.once('end', () => updateStateReaction.dispose())

        })
    }

    ...
}

Ina whiwhi taonga remote kua hangaia reaction ki te huri i te ahua e karanga ana i te mahi i te taha UI.

Ko te pa whakamutunga ko te taapiri i te whakaaturanga o nga karere hou ki te tohu toronga:

function setupApp() {
...

    // Reaction на выставление текста беджа.
    reaction(
        () => app.store.newMessages.length > 0 ? app.store.newMessages.length.toString() : '',
        text => extensionApi.browserAction.setBadgeText({text}),
        {fireImmediately: true}
    );

...
}

Na, kua rite te tono. Ka tono pea nga wharangi ipurangi ki te haina mo nga tauwhitinga:

Te tuhi toronga tirotiro haumaru

Te tuhi toronga tirotiro haumaru

Kei konei te waehere hono.

mutunga

Mena kua panui koe i te tuhinga tae noa ki te mutunga, engari kei a koe tonu nga patai, ka taea e koe te patai ki a raatau nga putunga me te toronga. I reira ka kitea ano e koe nga mahi mo ia taahiraa kua tohua.

A, ki te hiahia koe ki te titiro ki te waehere mo te toronga tuuturu, ka kitea e koe tenei konei.

Waehere, putunga me te whakaahuatanga mahi mai i siemarill

Source: will.com

Tāpiri i te kōrero